Министерство образования РФ Восточно-Сибирский Государственный Технологический Университет
Литвинов Д.Г.
Операционная ...
10 downloads
259 Views
532KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Министерство образования РФ Восточно-Сибирский Государственный Технологический Университет
Литвинов Д.Г.
Операционная система UNIX
Улан-Удэ, 2000 г.
Файловая система Linux Открытие и закрытие файлов В данном курсе рассматриваются лишь основополагающие методы работы с файловой системой, имеющиеся в подавляющем большинстве UNIX-систем. Рассмотрен только элементарный файловый ввод/вывод, а также некоторые современные методы работы с файлами, как отображение в память. Практически полностью опущен механизм файловой системы, позволяющий работать с каталогами, ссылками, атрибутами файлов. Системные функции работы с файлами классифицируются на несколько категорий, хотя некоторые из функций присутствуют более, чем в одной категории: • Системные функции, возвращающие дескрипторы файлов для использования другими системными функциями; • Системные функции анализирующие имя пути поиска; • Системные функции, назначающие и освобождающие индекс файла; • Системные функции, устанавливающие или изменяющие атрибуты файла; • Системные функции, позволяющие процессу производить ввод-вывод данных; • Системные функции, изменяющие структуру файловой системы; • Системные функции, позволяющие процессу изменять собственное представление о структуре дерева файловой системы. Открытие и закрытие файлов Ниже рассматриваются функции, позволяющие открыть и закрыть файлы, используя файловые дескрипторы. Функции creat и open определены в заголовочном файле , а функция close – в файле . int open (const char *FILENAME, int FLAGS[, mode_t MODE])
Функция создает и возвращает файловый дескриптор для файла, имя которого задано в FILENAME. После открытия файла указатель данных установлен на начало файла. Аргумент MODE используется только в том случае, если файл уже создан, однако передавать его в параметре можно в любом случае. Аргумент FLAGS задает способ, которым будет открыт файл. Он представляет собой битовую маску, значение может быть сформировано с помощью логического И. В случае нормального завершения функция возвращает целое положительное значение, представляющее собой дескриптор файла. В противном случае возвращается –1. Переменная errno может принимать следующие значения: EACCES Файл существует, но не может быть прочитан или записан способом, указанным аргументом FLAGS или файл не существует, а каталог не имеет разрешения на запись; EEXIST Флаги O_CREAT и O_EXCL, а файл уже существует; EINTR Операция открытия была прервана пришедшим сигналом; EISDIR FLAGS задает операцию записи, а файл является каталогом; EMFILE Процесс имеет слишком много открытых файлов. Максимальное значение открытых файлов задается макросом RLIMIT_NOFILE; ENFILE Операционная система или файловая система, в которой расположен файл не поддерживает дополнительных открытых файлов; ENOENT Заданный файл не существует, а флаг O_CREAT не установлен; ENOSPC Каталог или файловая система не может быть расширена по причине отсутствия дискового пространства; 2
ENXIO
Флаги O_NONBLOCK и O_WRONLY установлены, файл FILENAME является FIFO-файлом и ни один процесс не открыл файл на чтение; EROFS Файл размещается на файловой системе, предназначенной только для чтения, один из флагов O_WRONLY, O_RDWR или O_TRUNC, O_CREAT установлены и файл не существует. Если на 32-битном компьютере исходные коды программы компилировались с установленным флагом _FILE_OFFSET_BITS == 64, функция возвращает дескриптор файла, открытого в режиме “большого” файла, который позволяет использовать файлы размером до 263 байтов. Использование таких файлов незаметно для пользователя, так как вызовы функций заменяются на соответствующие, работающие с “большими” файлами. Функция не является повторно входимой, поэтому для ее использования необходимо использовать специальные механизмы, не рассматриваемые в данном курсе. int open64 (const char *FILENAME, int FLAGS[, mode_t MODE])
Функция идентична open за исключением того, что на 32-битных системах открывает файл в режиме “большого” файла. Если исходные тексты программы компилируются с флагом _FILE_OFFSET_BITS == 64, то эта функция вызывается при вызове open. int creat (const char *FILENAME, mode_t MODE)
Вызов этой функции идентичен вызову
open (FILENAME, O_WRONLY | O_CREAT | O_TRUNC, MODE)
При установленном флаге _FILE_OFFSET_BITS == 64 функция работает с “большими” файлами. Функция является устаревшей. int creat64 (const char *FILENAME, mode_t MODE)
Функция идентична creat, однако работает с “большими” файлами. Функция является устаревшей. int close (int FILEDES)
Функция закрывает файл, указанный дескриптором FILEDES. Закрытие файла имеет следующие последствия: • Освобождается память, ассоциированная с дескриптором; • Все захваченные записи разблокируются; • Когда дескриптор файла ассоциирован с каналом или FIFO-файлом, все непрочитанные записи теряются. В случае нормального завершения, функция возвращает значение 0, в противном случае возвращается –1. Переменная errno может принимать следующие значения: EBADF FILEDES не является правильным дескриптором файла; EINTR Функция была прервана поступлением сигнала ENOSPC При использовании сетевой файловой системы (NFS) эти ошибки, EIO возникающие при выполнении write не могут быть распознаны до EDQUOT вызова close. Изменение размера файла В некоторых случаях необходимо явно определить размер файла. Прототипы функций для выполнения этой операции расположены в заголовочном файле . int truncate (const char *NAME, off_t LENGTH)
Функция обрезает файл до размера, равного LENGTH байтов. Если размер файла был больше LENGTH, его размер становится равным LENGTH. Если размер был меньше или равен LENGTH, не выполняется никаких действий. Файл должен быть открыт на запись для выполнения этой операции.
3
Если при компиляции был установлен флаг _FILE_OFFSET_BITS == 64, вместо функции вызывается truncate64. При успешном выполнении возвращаемое значение равно 0, в противном случае возвращается –1, а переменная errno принимает одно из следующих значений: EACCES Файл не доступен пользователю; EINVAL Значение LENGTH недопустимо; EISDIR Файл является каталогом; ENOENT Файл не существует; ENOTDIR Каталог, заданный в пути файла NAME не существует. int truncate64 (const char *NAME, off64_t LENGTH)
Функция идентична truncate, за исключением того, что работает с 64-битными файлами. int ftruncate (int FD, off_t LENGTH)
Функция идентична truncate за исключением того, что вместо имени файла передается дескриптор уже открытого файла. Файл должен быть открыт на чтение. Если при компиляции был установлен флаг _FILE_OFFSET_BITS == 64, вместо ftruncate вызывается ftruncate64. В случае успешного выполнения возвращаемое значение равно 0, в противном случае возвращается –1. Переменная errno может принимать одно из следующих значений: EBADF FD не является дескриптором файла или файл не был открыт на запись; EINVAL Объект, указанный дескриптором не допускает выполнения такой операции; EROFS Файл расположен в файловой системе, предназначенной только для чтения. int ftruncate64 (int ID, off64_t LENGTH)
Функция идентична ftruncate за исключением того, что она работает с 64битными файлами. Ввод и вывод Следующие функции и структуры данных описаны в заголовочном файле . ssize_t
Этот тип данных указывает размер блоков, которые могут быть считаны или записаны за одну операцию. Он равен типу size_t, однако должен быть только положительным. ssize_t read (int FILEDES, void *BUFFER, size_t SIZE)
Функция считывает до SIZE байтов из файла, задаваемого дескриптором FILEDES и сохраняет результат в буфер BUFFER. Функция возвращает количество реально считанных байтов. Это значение может быть меньше, чем SIZE, например, в том случае, если размер файла меньше, чем SIZE. Нулевое количество считанных байтов означает конец файла. Значения, большие или равные 0, не являются ошибочными. В случае ошибки функция возвращает –1. Переменная errno может принимать следующие значения: EAGAIN В случае, если файл не имеет каких-либо данных для чтения, функция ожидает от записывающего процесса поступающих данных. Если был установлен флаг O_NONBLOCK, функция возвращает управление программе немедленно, а код ошибки принимает такое значение; EBADF Аргумент FILEDES не является дескриптором файла или файл не открыт для чтения; EINTR Выполнение функции было прервано поступившим сигналом; 4
EIO
Для многих устройств или дисковых файлов эта ошибка означает аппаратный сбой. Необходимо заметить, что в системе Linux нет функции read64, так как read не работает со смещениями в файле. ssize_t pread (int FILEDES, void *BUFFER, size_t SIZE, off_t OFFSET)
Функция идентична read за исключением того, что она считывает данные из файла не с текущей позиции указателя, а со смещения, указываемого в параметре OFFSET. Если компиляция выполнялась с установленным флагом _FILE_OFFSET_BITS == 64, вместо pread вызывается функция pread64. Функция возвращает количество реально считанных байтов и –1 в случае ошибки. Дополнительно к кодам ошибок pread могут возвращаться следующие: EINVAL Значение OFFSET является отрицательным и, соответственно, недопустимым; ESPIPE Файл, ассоциированный с дескриптором, является каналом или FIFOфайлом и не допускает позиционирования. OFFSET)
ssize_t pread64 (int FILEDES, void *BUFFER, size_t SIZE, off64_t
Функция идентична pread за исключением того, что работает с 64-битными файлами. ssize_t write (int FILEDES, const void *BUFFER, size_t SIZE)
Функция записывает SIZE байтов из буфера BUFFER в файл, заданный дескриптором FILEDES. Функция возвращает количество реально записанных байтов. Это значение может быть равно SIZE, однако всегда может быть равно и меньшему значению. Наилучшим решением проблемы записи в файл является вызовы функции в цикле до тех пор, пока все данные не будут записаны. При вызове write данные ставятся в очередь на запись и могут быть немедленно считаны, однако это не гарантирует того, что данные из временного хранилища были записаны на диск. Для того, чтобы удостоверится, что все данные были записаны на диск может быть вызвана функция fsync. В случае ошибки функция возвращает –1. Значения переменной errno может принимать следующие значения: EAGAIN Обычно функция write блокируется до тех пор, пока не закончена операция записи. Однако, если установлен флаг O_NONBLOCK, управление возвращается программе немедленно после вызова и возвращается такой код ошибки. Примером такой ситуации может служить запись на терминал, который поддерживает потоковое управление, в случае, если ему послан сигнал STOP остановки работы; EBADF FILEDES не является дескриптором файла или файл не открыт на запись; EFBIG Размер файла при записи может стать больше, чем позволяет операционная система; EINTR Операция была прервана сигналом; EIO Для многих устройств и дисковых файлов означает аппаратный сбой; ENOSPC Отсутствует свободное пространство на носителе информации, содержащем файл; EPIPE Ошибка возникает для каналов и FIFO-файлов, которые не открыты на чтение ни одним процессом.
5
В любом случае сбоя этой функции необходимо проверить значение errno. Если оно равно EINTR, нужно просто повторить вызов. Простейший способ выполнения этой операции – использование макроса TEMP_FAILURE_RETRY: OFFSET)
nbytes = TEMP_FAILURE_RETRY (write (desc, buffer, count)); ssize_t pwrite (int FILEDES, const void *BUFFER, size_t SIZE, off_t
Функция идентична pwrite за исключением того, что запись в файл выполняется не с текущей позиции, а с позиции, заданной OFFSET. В результате выполнения функции указатель на позицию в файле не изменяется. Если при компиляции установлен флаг _FILE_OFFSET_BITS == 64, вместо этой функции вызывается pwrite64. Функция возвращает количество реально записанных байтов. В случае ошибки возвращается –1, а переменная errno принимает следующие значения: EINVAL Значение OFFSET отрицательно и, следовательно, недопустимо; ESPIPE Дескриптор файла ассоциирован с каналом или FIFO-файлом, которые не допускают позиционирования указателя. ssize_t pwrite64 (int FILEDES, const void *BUFFER, size_t SIZE, off64_t OFFSET)
Эта функция идентична pwrite, за исключением того, что работает с 64битными файлами. Установка указателя на позицию в файле off_t lseek (int FILEDES, off_t OFFSET, int WHENCE)
Функция используется для изменения позиции указателя в файле, указываемом дескриптором FILEDES. Для получения текущей позиции указателя можно использовать следующий вызов: lseek (DESC, 0, SEEK_CUR)
Аргумент WHENCE используется для указания способа, которым необходимо изменить значение указателя. Он может принимать одно из следующих значений: SEEK_SET Показывает, что указатель вычисляется как смещение с начала файла; SEEK_CUR Показывает, что указатель вычисляется как смещение с текущего положения. Значение смещения может быть как положительным, так и отрицательным; SEEK_END Показывает, что указатель вычисляется как смещение с конца файла. Отрицательное значение смещения означает, что указатель вычисляется как (длина_файла – значение_смещения). Положительное значение переводит указатель за пределы файла. Если после этого выполнить операцию записи, то пространство до нового указателя будет заполнено нулями. Функция возвращает результирующее значение указателя после выполнения функции, измеряемое количеством байтов с начала файла. Если значение указателя не может быть изменено или операция не может быть выполнена по какой-либо причине, возвращается значение –1. Переменная errno может принимать следующие значения: EBADF FILEDES не является дескриптором файла; EINVAL Значение WHENCE или смещение является недопустимым; ESPIPE FILEDES указывает на объект, для которого позиционирования не допускается (например, канал, FIFO-файл, терминальное устройство). Если компиляция выполнялась с флагом _FILE_OFFSET_BITS == 64, вместо функции вызывает lseek64. off64_t lseek64 (int FILEDES, off64_t OFFSET, int WHENCE)
6
Функция идентична lseek за исключением того, что работает с 64-битными файлами. Возможно открыть несколько дескрипторов для одного и того же файла. В этом случае позиционирование указателя для одного дескриптора не влияет на другие. off_t
Это арифметический тип данных, используемый для представления размеров файлов. В Linux это значение равно long int. Если компиляция выполнялась с флагом _FILE_OFFSET_BITS == 64, этот тип данных заменяется off64_t. off64_t
Этот тип данных используется также, как и off_t. Ввод/вывод с отображением на память В современных операционных системах существует возможность отобразить файл на область в оперативной памяти. Когда это сделано, доступ к файлу можно получить как к обычному массиву. Этот способ более эффективен, чем использование read и write, так как можно загружать в память только те области файлы, которые необходимы программе. Механизм доступа к еще не загруженных в память участкам файла идентичен тому, который используется для виртуальной памяти при реализации свопинга. Так как обработанные участки файла могут быть сохранены обратно на диск, существует возможность отображать в память файлы гораздо большего размера, чем объем оперативной памяти. Единственным ограничителем является размер адресного пространства операционной системы. В 32-битной операционной системе теоретический лимит размера файла – 4 Gb, в действительности этот размер несколько меньше, так как некоторые участки памяти уже заняты операционной системой и другими программами. Отображение в память работает только с целыми страницами памяти. Поэтому адреса буферов для отображения должны быть выровнены на страницу, а длина округлена. Для определения размера страницы памяти можно использовать следующий вызов: size_t page_size = (size_t) sysconf (_SC_PAGESIZE);
Следующие функции определены в заголовочном файле <sys/mman.h>.
void * mmap (void *ADDRESS, size_t LENGTH,int PROTECT, int FLAGS, int FILEDES, off_t OFFSET)
Функция создает новое отображение в память участка файла FILEDES, ограниченного смещениями OFFSET и (OFFSET+LENGTH). ADDRESS задает предпочтительный начальный адрес буфера отображения. Если начиная с этого адреса существовало какое-либо другое отображения, оно уничтожается. Этот адрес может быть динамически изменен если не задан флаг MAP_FIXED. Аргумент PROTECT задает флаги, которые контролируют тип запрашиваемого доступа. Он может принимать следующие значения: PROT_READ, PROT_WRITE и PROT_EXEC, которые соответственно означают доступ на чтение, запись и исполнение. Заметим, что многие аппаратные системы не могут предоставить доступ только на запись без предоставления доступа на чтение. Аргумент FLAGS контролирует метод отображения и может принимать следующие значения: MAP_PRIVATE Означает, что изменения, вносимый в данные, хранящиеся в памяти никогда не будут отображены в соответствующем файле. Другие процессы, работающие с тем же файлом не смогут увидеть изменения, происходящие в файле. Такое отображение называется частным; 7
MAP_SHARED
Означает, что данные, измененные в памяти будут немедленно записаны в файл на диске. Такое отображение называется разделяемым; MAP_FIXED Указывает системе на использование точного адреса, передаваемого в ADDRESS и возвращать ошибку, если выполнить это невозможно; MAP_ANONYMOUS Создает анонимное отображение, не соединенное ни с каким MAP_ANON файлом. Анонимные отображения используются на некоторых системах для расширения размера области памяти, используемой для динамических переменных, могут использоваться также для передачи данных между приложениями без создания файла. В Linux функция выделения динамической памяти автоматически использует mmap. Флаги MAP_PRIVATE и MAP_SHARED не могут использоваться одновременно. Функция возвращает адрес буфера отображения в случае успешного выполнения и –1 в случае ошибки. Переменная errno может принимать следующие значения: EINVAL Либо ADDRESS не может быть использован либо заданы недопустимые значения FLAGS; EACCES Файл FILEDES не был открыт для доступа, заданного в PROTECT; ENOMEM Недостаточно памяти для выполнения операции или процесс имеет недостаточно памяти; ENODEV Файл имеет тип, не поддерживающий отображения; ENOEXEC Файл расположен в файловой системе, не поддерживающей отображения. int munmap (void *ADDR, size_t LENGTH)
Функция удаляет все отображения, расположенные в участках памяти от ADDR до (ADDR+LENGTH). Аргумент LENGTH должен в точности соответствовать длине буфера отображения. Безопасным является удаление нескольких отображений одной командой, включение в область памяти, не являющейся отображением или удаление только части отображения. Тем не менее, удалены будут только целые страницы памяти. В случае успешного выполнения функция возвращает 0, а в случае ошибки возвращается –1. Возможен единственный код ошибки: EINVAL Заданная область памяти не является отображением или не выровнена на страницу. int msync (void *ADDRESS, size_t LENGTH, int FLAGS)
При использовании разделяемых отображений, ядро операционной системы может записать в файл в любой момент. Чтобы удостоверится в том, что данные в памяти соответствуют данным на диске, используется эта функция. Она работает с областью памяти, задаваемой адресами ADDRESS и (ADDRESS+LENGTH). Функция может использоваться с одиночным отображением или с множественными отображениями. Участок памяти, не относящийся к отображению в этом случае задать нельзя. Аргумент FLAGS может принимать следующие значения: MS_SYNC Говорит о необходимости удостовериться в том, что данные в памяти соответствуют данным на диске; MS_ASYNC Говорит о необходимости начать процесс синхронизации, однако не 8
дожидается его окончания; Функция возвращает 0 в случае успешного выполнения и –1 в случае ошибки. Переменная errno может принимать следующие значения: EINVAL Была заданы недопустимая область памяти или значения флагов; EFAULT Нет существующего отображения по крайней мере в части задаваемой области памяти. FLAG)
void * mremap (void *ADDRESS, size_t LENGTH, size_t NEW_LENGTH, int
Функция используется для изменения размера существующей области памяти. Аргументы ADDRESS и LENGTH должны в точности задавать используемую в настоящий момент область памяти. Будет возвращена область отображения с теми же характеристиками, но с размером NEW_LENGTH. Аргумент FLAG может принимать единственное значение MREMAP_MAYMOVE, которое означает, что операционная система может уничтожить существующее отображение и создать новое требуемой длины, однако имеющий новый адрес. В случае успешного выполнения функция возвращает адрес буфера отображения, в случае возникновения ошибки возвращается –1. Переменная errno может принимать одно из следующих значений: EFAULT Отсутствует область отображения по крайней мере в части задаваемого буфера или участок памяти содержит более одного буфера отображения; EINVAL Адрес памяти не выровнен или недопустим; EAGAIN Участок памяти имеет захваченные участки и при расширении области, ограничение на захваченные страницы для данного будет превышено; ENOMEM Участок памяти является частным и открытым на запись и недостаточно виртуальной памяти для его расширения. Не все файловые дескрипторы могут быть отображены в память. Гнезда, каналы и большинство устройств позволяют получить только последовательный доступ к данным. Некоторые обыкновенные файлы также не могут быть отображены в память, устаревшие ядра операционных систем могут не поддерживать отображение. Синхронизация операций ввода-вывода В большинстве современных операционных систем обычные операции ввода/вывода не выполняются синхронно. Например, если даже операция write выполнилась нормально, это еще не означает, что данные фактически были записаны на носитель информации, например, жесткий диск. В ситуациях, когда необходима синхронизация выполняемых процессов, пользователь может использовать специальные функции, гарантирующие, что все операции ввода/вывода были завершены. int sync (void)
После вызова этой функции, она не возвращает управление в программу до тех пор, пока все данные не будут записаны на устройство. Прототип функции находится в заголовочном файле . Функция возвращает значение 0 при отсутствии ошибок. Гораздо чаще возникает ситуация, когда нет необходимости в синхронизации операций ввода/вывода всей операционной системы. Программе необходимо лишь удостовериться, что для данного файла все операции завершены. int fsync (int FILDES)
9
Функция может использоваться для того, чтобы удостовериться, что все данные, ассоциированные в файлом FILDES записаны на носитель. Функция не возвращает управление до тех пор, пока все операции не будут завершены. Прототип функции находится в заголовочном файле . Функция возвращает 0, если никаких ошибок не произошло и –1 в противном случае. Переменная errno может принимать следующие значения: EBADF Дескриптор FILDES не допустим; EINVAL Синхронизация недопустима, так как не поддерживается системой. Иногда нет необходимости даже в записи всех данных, ассоциированных с дескриптором. Например, в файлах баз данных, которые не изменяют свой размер, достаточно записать на диск только данные, хранящиеся в файле. Мета-информация, такая как время изменения файла, не так важна для восстановления данных в случае возникновения каких-либо проблем. Сохранение только несущих данных, очевидно, быстрее, чем сохранение всей информации, ассоциированной с дескриптором файла. int fdatasync (int FILDES)
Функция может использоваться для того, чтобы удостовериться, что все несущие данные, ассоциированные с дескриптором, записаны на носитель. Прототип функции находится в заголовочном файле . Функция возвращает 0 в случае успешного выполнения и –1 в случае ошибки. Глобальная переменная errno может принимать одно из следующих значений: EBADF Дескриптор файла недопустим; EINVAL Синхронизация недопустима, так как система ее не поддерживает. Контрольные операции над файлами Над дескрипторами файлов можно выполнять некоторые операции, например, получение или установка флагов, описывающих состояние дескриптора, захват блоков файла и тому подобное. Все эти операции выполняются функцией fcntl. int fcntl (int FILEDES, int COMMAND, ...)
Функция выполняет команду COMMAND над дескриптором файла FILEDES. Некоторые команды требуют задания дополнительных аргументов. Эти аргументы описываются в детальных описаниях команд. Приведем краткий список возможных команд: F_DUPFD Дублирование дескриптора файла. Возвращает другой дескриптор, ассоциированный с тем же открытым файлом; F_GETFD Получает флаги, ассоциированные с дескриптором; F_SETFD Устанавливает флаги, ассоциированные с дескриптором; F_GETFL Получает флаги, ассоциированные с открытым файлом; F_SETFL Устанавливает флаги, ассоциированные с открытым файлом; F_GETLK Получает информацию о захваченных областях файла; F_SETLK Устанавливает или сбрасывает захват файла; F_SETLKW Идентично F_SETLK, но ожидает окончания выполнения; F_GETOWN Получает процесс или группу процессов, которым будет послан сигнал SIGIO; F_SETOWN Устанавливает процесс или группу процессов, которым будет послан сигнал SIGIO; В случае ошибки функция, как правило, возвращает –1. Дублирование дескрипторов Существует возможность дублировать файловый дескриптор или выделить еще один дескриптор, ассоциированный с тем же файлом, что и исходный. Дублированные 10
дескрипторы один указатель позиции в файле, один набор флагов состояния файла, однако имеют собственные флаги дескриптора. Основной сферой использования дублирования является перенаправление ввода/вывода. Прототипы следующих функция находятся в заголовочном файле < unistd.h>. int dup (int OLD)
Функция копирует дескриптор OLD в первый доступный номер дескриптора файла. Функция эквивалентна вызову fcntl (OLD, F_DUPFD, 0). int dup2 (int OLD, int NEW)
Функция копирует дескриптор OLD в дескриптор NEW. Если OLD является недопустимым дескриптором, не выполняется никаких действий. В противном случае, новая копия OLD замещает значение NEW. Если NEW – дескриптор открытого файла, он закрывается перед дублированием. Приведем пример использования dup2 для перенаправления ввода/вывода. pid = fork (); if (pid == 0) { char *filename; char *program; int file; ... file = TEMP_FAILURE_RETRY (open (filename, O_RDONLY)); dup2 (file, STDIN_FILENO); TEMP_FAILURE_RETRY (close (file)); execv (program, NULL); }
Флаги дескрипторов файлов Флаги дескрипторов представляют собой различные атрибуты дескрипторов. При дублировании каждый дескриптор имеет собственный набор флагов. В настоящее время определен лишь один флаг дескриптора FD_CLOEXEC, означающий, что дескриптор должен быть закрыт в случае использования функции exec. Следующий макросы определены в файле . int F_GETFD Используется функцией fcntl для получения флага дескриптора. При задании этой команды функция fcntl возвращает значения флага; int F_SETFD Используется функцией fcntl для установки значения флага дескриптора. При вызове требуется третий аргумент и вызов выглядит таким образом: fcntl (FILEDES, F_SETFD, NEW-FLAGS)
Аргумент NEW-FLAGS имеет тип int. Приведем пример изменения флага дескриптора:
int set_cloexec_flag (int desc, int value) { int oldflags = fcntl (desc, F_GETFD, 0); /* Если произошла ошибка при чтении флага, выходим */ if (oldflags < 0) return oldflags; /* Устанавливаем только необходимый флаг */ if (value != 0) oldflags |= FD_CLOEXEC; else oldflags &= ~FD_CLOEXEC; /* Store modified flag word in the descriptor. */ return fcntl (desc, F_SETFD, oldflags); }
11
Флаги состояния файла Флаги состояния файла используются для указания атрибутов открытия файла. Флаги состояния файла задаются при открытии файла функцией open. Режимы открытия файла Режимы открытия файла дают возможность открыть файл на чтение, на запись или на чтение и запись одновременно. Флаги открытия файла определены в заголовочном файле . int O_RDONLY Открывает файл на чтение; int O_WRONLY Открывает файл на запись; int O_RDWR Открывает файл на чтение и запись. В Linux первые два флага могут быть объединены с помощью логического И и использоваться вместо O_RDWR. В Linux определены следующие дополнительные флаги, использование которых предпочтительно. int O_READ Открывает файл на чтение; int O_WRITE Открывает файл для записи; int O_EXEC Открывает файл для исполнения. Флаги открытия файла Флаги открытия файла определяют способ, которым функция open будет выполнять открытие файла. int O_CREAT Файл будет создан, если он не существует; int O_EXCL Если установлен вместе с предыдущим флагом, функция возвращает ошибку, если файл уже существует; int O_NONBLOCK Не используется блоковая передача данных; Следующие флаги существуют только в Linux. int O_IGNORE_CTTY Не воспринимает файл как контрольный терминал; int O_NOLINK Если файл является символьной ссылкой, открывается именно эта ссылка, а не файл, на который она ссылается; int O_TRUNC Делает размер файла равным нулю. Режимы ввода/вывода Следующие флаги определяют каким образом будут выполняться операции ввода/вывода над открытым файлом. Эти флаги устанавливаются функцией open и могут быть модифицированы с помощью fcntl. int O_APPEND Разрешает режим добавления данных в файл; int O_NONBLOCK Разрешает неблокируемый режим; int O_ASYNC Разрешает асинхронный режим ввода/вывода. Если установлен, генерируется сигнал SIGIO в случае возможности ввода; int O_FSYNC Разрешает синхронную запись в файл. Каждый вызов write не int O_SYNC возвращает управление в программу до тех пор, пока данные не будут записаны на диск; int O_NOATIME Если установлен, функция read не будет обновлять время последнего доступа к файлу. Получение и установка флага состояния файла Для выполнения этих операций используются следующие команды: int F_GETFL Используется для получения значения флага. При успешном 12
int F_SETFL
выполнении функция fcntl возвращает текущее значение флага; Используется для установки значения флага. При этом вызов fcntl выглядит следующим образом: fcntl (FILEDES, F_SETFL, NEW-FLAGS)
Аргумент NEW-FLAGS имеет тип int. Приведем пример изменения флага состояния файла:
int set_nonblock_flag (int desc, int value) { int oldflags = fcntl (desc, F_GETFL, 0); /* если произошла ошибка, выходим из программы */ if (oldflags == -1) return -1; /* устанавливаем только нужный флаг */ if (value != 0) oldflags |= O_NONBLOCK; else oldflags &= ~O_NONBLOCK; /* сохраняем флаг в дескрипторе файла */ return fcntl (desc, F_SETFL, oldflags); }
Захват файла Команды fcntl используются для поддержки «захвата записей», которая позволяет одновременно работающим программам независимо использовать только части одного и того же файла. Исключительный захват или захват на запись дают процессу доступ на запись в заданный участок файла. Пока участок файла захвачен никакой другой процесс не может этот участок на запись. Разделяемый захват или захват на чтение запрещает установку каким-либо процессом захвата участка файла на запись. Однако любое количество процессов могут захватывать участок на чтение. Функции read и write не проверяют существует ли в настоящий момент какойлибо захват. Поэтому для реализации протокола захвата для файла, разделяемого многими процессами, необходимо выполнять явные вызовы fcntl для запроса и освобождения захватов. Захваты ассоциируются с процессами. Процесс может иметь только один тип захвата для каждого байта файла. Когда любой дескриптор файла закрывается процессом, то все захваты освобождаются даже в том случае, если они выполнялись через другие дескрипторы. Захваты освобождаются когда процесс закрывается и не наследуются процессами, созданными с помощью функции fork. Для задания захвата используется структура flock, которая специфицирует тип захвата и другие параметры. Структура и соответствующие макросы описаны в заголовочном файле . struct flock { short int l_type; short int l_whence; off_t l_start; off_t l_len; pid_t l_pid; };
Приведем значения полей структуры. Задает тип захвата. Может принимать значения F_RDLCK, F_WRLCK или F_UNLCK; l_whence Соотвествует аргументу WHENCE функции lseek. Может принимать значения SEEK_SET', SEEK_CUR или SEEK_END; l_type
13
l_start
Указывает смещение начала области, на которую распространяется захват; l_len Задает длину области, на которую распространяется захват; l_pid Задает идентификатор процесса, которому принадлежит захваченная область. Следующие макросы используются для задания команд и параметров захвата. int F_GETLK Используется как параметр COMMAND функции fcntl и указывает, что необходимо получить информацию о захвате. При этом функция принимает третий параметр типа struct flock *, вызов функции выглядит следующим образом: fcntl (FILEDES, F_GETLK, LOCKP)
Если захват в заданной области файла существует, в структуре возвращается информация о нем. В области может существовать более одного захвата. В этом случае информация возвращается только об одном; int F_SETLK Используется в функции fcntl как команда. При этом функция принимает третий аргумент типа struct flock *. Если в заданной области уже существует захват, старый захват замещается новым. Удалить захват можно задав тип захвата F_UNLCK; int F_SETLKW Аналогичен предыдущему за исключением того, что процесс блокируется до тех пор, пока команда не будет выполнена; F_RDLCK Задает тип захвата на чтение; F_WRLCK Задает тип захвата на запись; F_UNLCK Область разблокируется. Примером ситуации, в которой может быть полезен захват файла служит быть программа, которая может быть запущена несколькими пользователями независимо, записывающая протокол работы в файл. Несколько процессов, одновременно записывающих информацию в файл могут смещать информацию различных пользователей, что приведет к неразберихе. В случае захвата файла этого не произойдет. Процессы Процессы являются основными единицами, применяемыми для выделения системных ресурсов. Каждый процесс имеет собственное адресное пространство и (обычно) один поток управления. Процесс исполняется программой; можно запустить несколько процессов, выполняющих одну и ту же программу, но каждый процесс имеет свою собственную копию программы и исполняет ее независимо от других. Процессы организованы иерархически. Каждый процесс имеет «родительский» процесс, который явно классифицируется как его создатель. Процессы, созданные данным «родителем» называются «дочерними» процессами. Дочерний процесс наследует многие атрибуты своего родителя. В этом разделе описывается каким образом программа может создать, завершить и управлять дочерними процессами. Фактически существует три выполняемые задачи: 1. Создание процесса; 2. Выполнение новым процессом программы; 3. Координация завершение дочернего процесса с родительским процессом.
14
Запуск команды Наиболее простым способом запустить программу является использование функции system. Эта функция выполняет большую часть работы по запуску программы, однако не дает практически никаких возможностей по управлению деталями – программа вынуждена ждать завершения подпрограммы перед тем, как программа сможет сделать что-либо еще. int system (const char *COMMAND)
Эта функция выполняет команду COMMAND как команду командного процессора. В Linux всегда используется командный процессор sh. Он пытается найти команду в каталогах, указанных в переменной среды PATH и выполняет найденную команду. Функция возвращает –1 в случае, если создать процесс невозможно и код состояния процесса в случае успешного выполнения. Если аргумент COMMAND является нулевым указателем, то ненулевое возвращаемое значение показывает, что командный процессор существует и функция может использоваться. Прототип функции определен в заголовочном файле <stdlib.h>. Концепции создания процесса Каждому процессу при создании присваивается свой идентификатор процесса (ID), который является уникальным среди всех идентификаторов процессов. Время жизни процесса заканчивается в момент, когда сообщение о прекращении работы дочернего процесса передается родительскому процессу. В этот момент все ресурсы, ассоциированные с процессом освобождаются. Обычно процессы создаются с помощью функции fork. Дочерний процесс, созданный с помощью fork является копией родительского процесса за тем исключением, что имеет собственный идентификатор. После создания дочернего процесса оба процесса продолжают выполняться в нормальном режиме. Если существует необходимость дождаться окончания выполнения дочернего процесса, а лишь за этим продолжить выполнять поток родительского процесса, то для этого можно использовать функции wait и waitpid. Вновь созданный дочерний процесс выполняет ту же самую программу, что и родительский процесс, начиная с той точки, где вернула управление функция fork. Значение, возвращаемое fork может использоваться для того, чтобы определить, выполняется программа в родительском или дочернем процессе. Выполнение одной и той же программы несколькими процессами редко является полезным. Однако, дочерний процесс может выполнять другую программу, если он создан функцией exec. Программа, которая исполняется процессом, называется «изображением процесса». Идентификация процесса С помощью типа данных pid_t можно задать идентификаторы процессов. Можно получить идентификатор процесса, выполнив функция getpid. Функция getppid возвращает идентификатор родительского процесса. Следующие тип данных и функции определены в заголовочных файлах и <sys/types.h>. pid_t
Этот знаковый целочисленный тип данных предназначен для представления идентификаторов процессов. В Linux он равен типу данных int. pid_t getpid (void)
Функция возвращает значение идентификатора текущего процесса. pid_t getppid (void)
Функция возвращает значение идентификатора родительского процесса. 15
Создание процесса pid_t fork (void)
Функция создает новый процесс. Прототип функции определен в заголовочном файле . Если операция выполнилась успешно, то выполнение и родительского и дочернего процессов продолжается начиная с команды, следующей за fork. В случае дочернего процесса fork возвращает значение 0, а в случае родительского процесса – идентификатор порожденного процесса. Если создание процесса выполнилось неудачно, функция возвращает –1 в родительском процессе. Переменная errno может принимать следующие значения: EAGAIN Недостаточно системных ресурсов для создания нового процесса или пользователь запустил слишком много процессов. Это означает превышение значения RLIMIT_NPROC, которое обычно может быть увеличено; ENOMEM Процессу требуется больше пространства памяти, чем система может предоставить. Некоторые атрибуты дочернего процесса отличаются от атрибутов родительского процесса: • Дочерний процесс имеет собственный идентификатор; • Дочерний процесс получает собственные копии открытых файловых дескрипторов родительского процесса. Однако изменение атрибутов дескрипторов дочернего процесса никак не влияет на родительский и наоборот; • Процессорное время дочернего процесса в момент создания устанавливается равным нулю; • Дочерний процесс не наследует захваты файлов родительского процесса; • Дочерний процесс не наследует аварийных сигналов, установленных родительским процессом; pid_t vfork (void)
Идентична fork, но на некоторых системах выполняется более эффективно. Однако существуют условия, только при выполнении которых можно использовать функцию безопасно. В отличие от fork, vfork не создает копию родительского процесса, а создает разделяемое с родительским процессом адресное пространство до тех пор, пока не будет вызвана функция _exit или одна из функций exec. Родительский процесс на это время останавливает свое выполнение. Отсюда следуют и все ограничения на использование – дочерний процесс не может изменять никакие глобальные переменные или даже общие переменные, разделяемые с родительским процессом. В Linux функция vfork не реализована, вместо нее выполняется fork. Исполнение файла Здесь описывается применение семейства функций exec, предназначенных для исполнения файла как изображения процесса. Эти функции могут использоваться для того, что использовать в процессе какую-либо программу уже после того, как он был создан. Все функции отличаются использованием параметров, однако в действительности они выполняют одно и то же. Прототипы функций определены в заголовочном файле . int execv (const char *FILENAME, char *const ARGV[])
16
Функция исполняет файл, заданный строкой FILENAME как новое изображение процесса. Аргумент ARGV представляет собой массив строк, заканчивающихся нулем, которые используются как аргумент argv функции main. Последним элементом этого массива должен быть нулевой указатель. Первый элемент массива – имя файла программы. Среда нового изображения процесса берется из переменной environ текущего изображения процесса. int execl (const char *FILENAME, const char *ARG0, ...)
Идентична execv, однако все аргументы программы передаются в функцию индивидуально вместо задания массива. ENV[])
int execve (const char *FILENAME, char *const ARGV[], char *const
Идентична execv за исключением того, что процессу передаются переменные среды в аргументе ENV. int execle (const char *FILENAME, const char *ARG0, char *const ENV[], ...)
Идентична предыдущей функции за исключением того, что аргументы программы передаются индивидуально. int execvp (const char *FILENAME, char *const ARGV[])
Идентична execv за исключением, что пытается найти команду с таким именем в каталогах, указанных в переменной среды PATH, если FILENAME не содержит ни одного символа слэша. int execlp (const char *FILENAME, const char *ARG0, ...)
Идентична execl за исключением того, что выполняет поиск команды аналогичный предыдущей функции. Размер аргументов и переменных среды не должен превышать ARG_MAX байтов. В Linux размер строки вычисляется как длина строки + размер типа данных char * +1. Функция не возвращают управление в родительскую программу до тех пор, пока не дочерний процесс не прекратит выполнение. В случае ошибки функция возвращает значение –1. Переменная errno может принимать одно из следующих значений: E2BIG Совместная длина аргументов программы и переменных среды превышает E2BIG байтов; ENOEXEC Указанный файл не может быть исполнен, так как имеет неверный формат; ENOMEM Исполнение программы требует больший размер оперативной памяти, чем доступно. Выполнение нового изображения процесса полностью меняет содержимое оперативной памяти, однако многие атрибуты процесса остаются неизменными: • Идентификатор процесса и идентификатор родительского процесса; • Сессия и членство в группе процессов; • Идентификатор пользователя и идентификатор группы; • Текущий рабочий каталог и корневой каталог; • Маска режима создания файла; • Маска сигналов процесса; • Время выполнения процесса. Файловые дескрипторы открытых файлов остаются открытыми, если для дескриптора не задан флаг FD_CLOEXEC.
17
Завершение процесса Следующие функции используются для ожидания завершения работы или остановки работы дочернего процесса. Они определены в заголовочном файле <sys/wait.h>. pid_t waitpid (pid_t PID, int *STATUS-PTR, int OPTIONS)
Используется для запроса информации о состоянии дочернего процесса, идентификатор которого равен PID. Обычно вызывающий процесс приостанавливает выполнение до тех пор, пока дочерний процесс не завершит выполнение. Аргумент PID может принимать и другие значения. Значение –1 означает ожидание любого процесса, 0 – запрашивает информацию о процессах, находящихся в той же группе процессов. Если информация о состоянии процесса доступна немедленно, функция не ожидает окончания выполнения процесса. Если запрошен статус нескольких процессов, то возвращается статус случайного процесса. Аргумент OPTIONS является битовой маской, значение которой может формироваться из следующих макросов: WNOHANG Родительский процесс не должен ожидать завершения дочернего; WUNTRACED Запрашивается информация об остановленных и завершенных процессах. Информация о состоянии сохраняется в объекте, на который указывает STATUS-PTR если он не является нулевым указателем. В случае нормального завершения функция возвращает идентификатор процесса, для которого запрашивается состояние, иначе возвращается –1. Переменная errno может принимать следующие значения: EINTR Выполнение функции прервалось поступившим сигналом; ECHILD Отсутствуют дочерние процессы или процесс, идентификатор которого передан в функции не является дочерним; EINVAL Неверное значение аргумента OPTIONS. pid_t wait (int *STATUS-PTR)
Упрощенная версия предыдущей функции. Ожидает завершение любого дочернего процесса. Вызов wait (&status)
эквивалентен вызову
waitpid (-1, &status, 0)
Приведем пример использования waitpid для получения состояния всех завершенных дочерних процессов бех ожидания. void sigchld_handler (int signum) { int pid, status, serrno; serrno = errno; while (1) { pid = waitpid (WAIT_ANY, &status, WNOHANG); if (pid < 0) { perror ("waitpid"); break; } if (pid == 0) break; notice_termination (pid, status); } errno = serrno; }
18
Пример создания процесса Ниже приведен пример того, каким образом можно написать функцию, идентичную системной функции system. Она выполняет команду используя эквивалент вызова sh –c COMMAND. #include #include #include #include #include
<stddef.h> <stdlib.h> <sys/types.h> <sys/wait.h>
/* Execute the command using this shell program. #define SHELL "/bin/sh"
*/
int my_system (const char *command) { int status; pid_t pid;
}
pid = fork (); if (pid == 0) { /* это дочерний процесс. Выполняем команду */ execl (SHELL, SHELL, "-c", command, NULL); _exit (EXIT_FAILURE); } else if (pid < 0) /* создание процесса невозможно */ status = -1; else /* родительский процесс. Ожидаем завершения */ if (waitpid (pid, &status, 0) != pid) status = -1; return status;
Сигналы Сигналом называется программное прерывание, передаваемое выполняемому процессу. Операционная система использует сигналы для информирования выполняемых процессов об исключительных ситуациях. Все события, генерирующие сигналы могут быть разбиты на три основные категории: ошибки, внешние события и явные вызовы. Ошибка означает, что программа выполнила некорректную операцию и не может быть продолжена. К ошибкам, которые могут генерировать сигналы относятся ошибки деления на ноль и ошибки обращения к неверным адресам памяти. Внешнее событие вызывается как правило устройствами ввода-вывода или другими процессами. Непосредственный вызов означает использование библиотечной функции (например, kill), непосредственным назначением которой является генерация сигнала. Сигналы могут генерироваться синхронно и асинхронно. Синхронные сигналы свойственны некоторым действиям, выполняемым программой и выполняются непосредственно во время этого действия. К синхронным сигналам относятся большинство сигналов, сообщающих об ошибках. Асинхронные сигналы генерируются событиями, происходящими вне выполняемого процесса и поступают в неизвестный заранее момент времени. Когда сигнал генерируется, он помещается операционной системой в очередь ожидания и через некоторое время передается процессу для обработки. Существует
19
возможность блокировки сигналов. В этом случае сигнал находится в состоянии ожидания до тех пор, пока он не будет разблокирован. Для некоторых сигналов выполняемые действия заранее предопределены. Однако, при обработке большинства сигналов процесс имеет альтернативу: игнорировать сигнал, определить функцию-обработчик или вызывать обработчик по умолчанию. Стандартные сигналы Все стандартные сигналы определены в заголовочном файле <signal.h>. Каждый сигнал задается с помощью макроса, задающего целочисленный положительный идентификатор. Сигналы программных ошибок Эти сигналы генерируются когда операционной системой или компилятором обнаруживается серьезная программная ошибка и продолжение работы невозможно. Действие по умолчанию – завершение работы процесса. Сигналы программных ошибок могут перехватываться для очистки памяти или освобождения ресурсов перед прекращением работы. SIGFPE Фатальная арифметическая ошибка. Сигнал вызывается при всех арифметических ошибках, например делении на ноль или переполнении. SIGILL Неверная инструкция. Причины генерации сигнала: попытка выполнения неправильной команды процессора или привилегированной инструкции (например, в случае разрушения выполняемого файла или попытки выполнения данных), переполнение стека, неправильное определение перехватчика сигналов. SIGSEGV Генерируется когда программа пытается обратиться к памяти вне выделенного блока памяти или записать в область памяти, из которой возможно только чтение. SIGBUS Генерируется при попытке манипуляции с заведомо неверным адресом памяти. SIGABRT Указывает на ошибку, обнаруженную самой программой и обработанную с помощью вызова функции abort. SIGTRAP Вызывается машинной инструкцией точки останова и, возможно, другими отладочными командами. Используется отладчиками. SIGSYS Неверный системный вызов. Генерируется, когда выполняется инструкция операционной системы, однако задан неверный номер инструкции. Сигналы завершения Сообщают процессу о необходимости завершиться. Процесс может обрабатывать этот сигнал если перед завершением выполнения нужно, например, очистить используемый участок памяти или удалить временные файлы. Действие, выполняемое по умолчанию – завершение процесса. SIGTERM Сигнал, используемый для того, чтобы вызвать прекращение выполнения процесса. В отличие от сигнала SIGKILL может быть заблокирован, обработан процессом или проигнорирован. SIGINT Прерывание процесса. Обычно вызывается при нажатии пользователем комбинации клавиш CTRL+C. SIGQUIT Прерывание процесса. В отличие от SIGINT вызывается комбинацией
20
CTRL+\. SIGKILL Сигнал, используемый для немедленного прерывания выполнения процесса. Не может быть обработан, проигнорирован или заблокирован. SIGHUP Информирует о том, что соединение с пользовательским терминалом потеряно из-за ошибки сети или телефонной линии. Сигналы таймера Сигналы используются для информирования о событиях, приходящих от таймеров. Действие, выполняемое по умолчанию – завершение процесса. SIGALRM Информирует об окончании интервала реального времени, отсчитываемого таймером. Используется, например, функцией alarm. SIGVTALRM Информирует об окончании интервала процессорного времени, используемого текущим процессом, отсчитываемого таймером. SIGPROF Информирует об окончании интервала как процессорного времени, отсчитываемого таймером как для текущего процесса, так и для времени, затрачиваемом операционной системой на работу с этим процессом. Асинхронные сигналы ввода-вывода Используются вместе с асинхронными устройствами ввода-вывода. Действие, выполняемое по умолчанию – игнорирование сигнала. С помощью вызова функции fcntl можно заставить дескриптор файла ввода-вывода генерировать сигналы этого типа. SIGIO Посылается, когда дескриптор файла готов к чтению или записи данных. SIGURG Посылается, когда на устройство посылает какой-либо экстренный блок данных (например, сообщение о фатальной ошибке). Сигналы управления заданиями SIGCHLD Посылается родительскому процессу когда один из порожденных процессов прерывает свое выполнение. SIGCONT Может быть послан процессу для того, чтобы продолжить его выполнение. SIGSTOP Останавливает процесс. Не может быть заблокирован, перехвачен или проигнорирован. SIGTSTP Аналогичен SIGSTOP, однако может быть перехвачен и проигнорирован. Сигналы об ошибках операционной системы SIGPIPE Нарушенный канал. Генерируется, например, при попытке процессом записать данные в несуществующий канал. SIGLOST Потерян ресурс. Возникает, например, при обращении процесса к файлу на диске NFS, если с сервером NFS потеряна связь. В Linux вызывается при любом аварийном завершении серверной программы. SIGXCPU Превышение лимита на процессорное время. SIGXFSZ Превышение лимита на размер файла. Вызывается при попытке увеличить размер файла свыше лимита, определенного операционной системой. Другие сигналы SIGUSR1, Пользовательские сигналы, которые могут использоваться для SIGUSR2 внутренних целей программы. SIGWINCH Изменение размера окна. В Linux генерируется при изменении 21
количества строк и столбцов терминального окна. Сообщения сигналов За каждым сигналом закреплено определенное текстовое сообщение, описывающее причину его возникновения. Для получения этого сообщения в Linux предусмотрены следущие функции: char * strsignal(int SIGNUM)
Возвращает указатель на статически размещенную строку, описывающую сигнал SIGNUM. void psignal(int SIGNUM, char *MESSAGE)
Печатает текстовое сообщение в стандартный поток вывода. ‘stderr’. Если MESSAGE не является нулевым указателем, функция предваряет сообщение содержимым этой строки. Обе функции определены в заголовочном файле <signal.h>. Определение обработчика сигнала Наиболее простым способом определить обработчик сигнала является использование функции signal. Ее определение находится в заголовочном файле <signal.h>. sighandler_t signal(int SIGNUM, sighandler_t ACTION)
sighandler_t – тип функций обработки сигналов (используется только в Linux). Таким образом, можно определить обработчик следующим образом: void HANDLER (int signum) {. . .}
Аргумент SIGNUM определяет сигнал, который будет обрабатываться. Сигнал необходимо указывать в виде символьного обозначения, так как соответствующие числовые идентификаторы могут различаться в различных операционных системах. Аргумент ACTION указывает на действие, которое будет выполняться при появлении сигнала. Аргумент может принимать следующие значения: SIG_DFL Действие по умолчанию. SIG_IGN Сигнал игнорируется. HANDLER Определяет адрес функции-обработчика. Функция signal возвращает то действие, которое использовалось для заданного сигнала до выполнения функции. При возникновении ошибки возвращается значение SIG_ERR, при задании неправильного идентификатора сигнала возвращаемое значение равно EINVAL. Пример использования обработчика сигналов для удаления временных файлов при фатальных ошибках: #include <signal.h> void termination_handler(int signum) { struct temp_file *p; for (p = temp_file_list; p; p = p->next()) unlink(p->name); } int main(void) { . . . if (signal(SIGINT, termination_handler) == SIG_IGN) signal(SIGINT, SIG_IGN); if (signal(SIGHUP, termination_handler) == SIG_IGN) signal(SIGINT, SIG_IGN);
22
}
if (signal(SIGTERM, termination_handler) == SIG_IGN) signal(SIGTERM, SIG_IGN); . . .
Функция sigaction имеет то же назначение, что и signal, однако предоставляет больший контроль над процессом обработки сигналов. Функция определена в заголовочном файле <signal.h>. int sigaction(int SIGNUM, const struct sigaction *ACTION, struct sigaction *OLD_ACTION)
Структура sigaction определяется следующим образом: struct sigaction { int sa_flags; sighandler_t sa_handler; sigset_t sa_mask; };
sa_handler используется таким же образом, как и аргумент ACTION в функции signal; sa_mask задает список сигналов, которые должны быть заблокированы при выполнении обработчика; sa_flags задает значения флагов, которые влияют на поведение сигнала. Аргумент ACTION задает новый обработчик для сигнала, OLD_ACTION используется для того, чтобы возвратить информацию об обработчике, ассоциированном с сигналом. Если OLD_ACTION является нулевым указателям, информация о предыдущем обработчике не возвращается. Если ACTION является нулевым указателем, обработчик сигнала не изменяется. Функция возвращает следующие значения: 0 в случае успеха; -1 в случае ошибки; EINVAL в случае неправильно заданного идентификатора сигнала. Пример использования функции sigaction: #include <signal.h> void termination_handler (int signum) { struct temp_file *p;
}
for (p = temp_file_list; p; p = p->next) unlink (p->name);
int main (void) { ... struct sigaction new_action, old_action; new_action.sa_handler = termination_handler; sigemptyset (&new_action.sa_mask); new_action.sa_flags = 0; sigaction (SIGINT, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) sigaction (SIGINT, &new_action, NULL); sigaction (SIGHUP, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) sigaction (SIGHUP, &new_action, NULL); sigaction (SIGTERM, NULL, &old_action);
23
}
if (old_action.sa_handler != SIG_IGN) sigaction (SIGTERM, &new_action, NULL); ...
Функция sigemptyset будет описана позднее. Значение аргумента sa_flags интерпретируется как битовая маска. Аргумент может включать следующие флаги: SA_NOCLDSTOP действует только для сигнала SIGCHLD. Если флаг установлен, сигнал не посылается тем порожденным процессам, которые остановлены. SA_ONSTACK если флаг установлен, операционная система использует стек для передачи сигнала. Если для процесса не определен стек, а сигнал был сгенерирован, то процесс завершается с выработкой сигнала ‘SIGILL’. SA_RESTART Флаг контролирует действие, которое происходит, если генерируется какой-либо сигнал в результате работы функции (например, open, write, read) и обработчик сигнала выполнился нормально. Если флаг установлен, по окончании выполнения обработчика продолжает выполняться вызванная функция, иначе выполнение функции прекращается. Разработка обработчиков сигналов Существуют две базовые стратегии, используемые при разработке функцийобработчиков: 1. при поступлении сигнала функция выполняет некоторое действие и возвращает управление в программу; 2. функция прерывает выполнение программы или исправляет ситуацию, возникшую в результате ошибки. Следует обратить особое внимание на то, что обработчик может быть вызван асинхронно, из любой точки программы. При очень коротком интервале времени выдачи сигнала два и более процессов обработчиков могут выполняться одновременно. Обработчики первого типа используются для сигналов ‘SIGALRM’, сигналов, поступающих от устройств ввода-вывода. Обработчик должен изменять некоторую глобальную переменную в знак того, что обработчик получил управление. Тип данных этой переменной должен быть sig_atomic_t. Пример обработчика сигнала: #include #include #include volatile
<signal.h> <stdio.h> <stdlib.h> sig_atomic_t keep_going = 1;
void catch_alarm (int sig) { keep_going = 0; signal (sig, catch_alarm); } void do_stuff (void) { puts ("Doing stuff while waiting for alarm...."); }
24
int main (void) { signal (SIGALRM, catch_alarm); alarm (2); while (keep_going) do_stuff (); }
return EXIT_SUCCESS;
Обработчики второго типа обычно используются для выполнения некоторых завершающих действий при критических ошибках. Наилучший путь для прекращения выполнения процесса – вызвать тот же сигнал с определенным действием по умолчанию. Например, volatile sig_atomic_t fatal_error_in_progress = 0; void fatal_error_signal (int sig) { if (fatal_error_in_progress) raise (sig); fatal_error_in_progress = 1; ...
}
signal (sig, SIG_DFL); raise (sig);
Если запущен обработчик для конкретного сигнала, то этот сигнал блокируется до тех пор, пока работа обработчика не будет завершена. Тем не менее, работа обработчика может быть прервана приходом какого-либо другого сигнала. Для того, чтобы избежать этого нужно использовать поле sa_mask структуры sigaction, чтобы указать какие сигналы будут заблокированы в процессе выполнения обработчика. При написании обработчиков сигналов следует стремиться к минимизации его кода и сокращению времени выполнения. Критическим местом могут стать структуры данных, с которыми работает обработчик. Необходимо придерживаться следующих принципов: • если обработчику необходимо работать с глобальной переменной, определенной в программе, то она должна быть определена как volatile, что говорит компилятору о возможности изменения ее асинхронно; • если в обработчике вызывается какая-либо функция, убедитесь, что она является повторно входимой или в том, что сигнал не может прервать выполнение этой функции. Элементарный доступ к данным При написании обработчиков существует проблема обращения к неэлементарным данным (объектам в памяти, доступ к которым может быть осуществлен лишь при выполнении нескольких операций с данными). В случае поступления сигнала в момент модификации содержимого объекта, его значение может стать непредсказуемым. Например, #include <signal.h> #include <stdio.h>
25
struct two_words { int a, b; } memory; void handler(int signum) { printf ("%d,%d\n", memory.a, memory.b); alarm (1); } int main (void) { static struct two_words zeros = { 0, 0 }, ones = { 1, 1 }; signal (SIGALRM, handler); memory = zeros; alarm (1); while (1) { memory = zeros; memory = ones; } }
1. 2.
Существует два способа решить эту проблему: использование элементарного типа данных. Таким типом данных является sig_atomic_t. В общем случае, тип данных int и все типы данных, размерностью меньшей, чем int являются элементарными; обработка данных только в теле программы. При этом обработчик должен только модифицировать флаг, используемый для синхронизации программы и обработчика.
Генерация сигналов Процесс может послать сигнал самому себе с использованием следующей функции. int raise(int SIGNUM)
Функция посылает сигнал вызывающему процессу. Возвращает 0 в случае успеха и ненулевое значение в противном случае. Единственной возможностью получения неблагоприятного результата является неверное значение SIGNUM. В частности, raise может использоваться для выполнения действий, предписанных сигналу по умолчанию. Посылка сигналов другим процессам Для посылки сигналы другим процессам может использоваться функция kill. Она может использоваться для выполнения множества различных действий. Необходимость посылать сигналы другим процессам возникает, например, в следующих случаях: • родительский процесс запускает порожденный для выполнения некоторой задачи, причем выполняющий неопределенный цикл, и завершает его, когда в дальнейшем выполнении задачи нет необходимости; • процесс выполняется как один из процессов в группе и нуждается в прерывании или информировании других процессов при возникновении ошибки или какого-либо критического события; • два процесса необходимо синхронизировать при одновременном выполнении. int kill(pid_t PID, int SIGNUM)
26
Функция kill посылает сигнал SIGNUM процессу или группе процессов, задаваемому PID. PID может принимать следующие значения: >0 сигнал посылается процессу, идентификатор которого равен PID; == 0 сигнал посылается всем процессам, которые находятся в той же группе процессов, что и процесс-отправитель; < -1 сигнал посылается группе процессов, идентификатор которой равен PID; == -1 если процесс является привилегированным, то сигнал посылается всем процессам, кроме некоторых системных процессов. Иначе, сигнал посылается всем процессам, владельцем которых является текущий пользователь. Если сигнал успешно послан, то kill возвращает 0, иначе сигнал не посылается и функция возвращает –1. В случае неудачи в переменной errno возвращаются следующие коды: EINVAL аргумент SIGNUM содержит неправильный номер сигнала; EPERM отсутствуют привилегии на посылку сигнала процессу или группе процессов; ESCRH аргумент PID не соответствует ни одному из существующих процессов или групп процессов. Процесс может послать сигнал самому себе с помощью вызова kill(getpid(), SIG), что равнозначно raise(SIG).
• • •
Блокирование сигналов Блокирование сигналов может быть полезно в следующих случаях: временное блокирование сигналов дает способ избавиться от прерываний во время выполнения критичной части программы; для того, чтобы сделать программе надежнее, можно заблокировать сигналы на время модификации данных; единственный способ проверки появления сигнала – его блокировка. Все функции блокировки сигналов используют структуру данных sigset_t, называемую “набором сигналов”, для указания какие сигналы будут заблокированы. Для задания множества сигналов используются следующие функции. В целях безопасности для работы со структурой sigset_t рекомендуется использовать только описываемые функции. int sigemptyset(sigset_t *SET)
Функция исключает из множества заблокированных определенные сигналы. Всегда возвращает 0.
все
Функция включает во множество заблокированных определенные сигналы. Всегда возвращает 0.
все
int sigfullset(sigset_t *SET)
int sigaddset(sigset_t *SET, int SIGNUM)
EINVAL
Функция добавляет сигнал SIGNUM в набор сигналов SET. Возвращает 0 в случае успешного выполнения и –1 в случае ошибки. При возникновении ошибки переменная errno может принимать следующее значение: SIGNUM содержит неправильный номер сигнала.
int sigdelset(sigset_t *SET)
Функция удаляет сигнал SIGNUM из набора сигналов SET. Возвращаемые значения такие же, как и для функции sigaddset. int sigismember(const sigset_t *SET, int SIGNUM)
27
Функция проверяет входит ли сигнал SIGNUM в набор сигналов SET. Возвращает 1, если сигнал находится в наборе и 0 в противном случае, -1 – в случае ошибки выполнения. При возникновении ошибки переменная errno может принимать следующее значение: EINVAL SIGNUM содержит неправильный номер сигнала. Набор сигналов, который заблокирован в данный момент, называется “маской сигналов”. Каждый процесс имеет свою собственную маску сигналов. При создании нового процесса, он наследует маску сигналов родительского процесса. Для модификации маски сигналов используется следующая функция: int sigprocmask(int HOW, const sigset_t *SET, sigset_t *OLDSET)
Аргумент HOW определяет каким образом изменяется маска сигналов и может принимать следующие значения: SIG_BLOCK сигналы, задаваемые в наборе, блокируются – добавляются к уже существующей маске сигналов; SIG_UNBLOCK сигналы, задаваемые в наборе, разблокируются – удаляются из уже существующей маски сигналов процесса; SIG_SETMASK устанавливает набор сигналов для процесса, старое содержимое маски игнорируется. Аргумент OLDSET используется для возврата старого содержимого маски сигналов процесса. Функция возвращает 0 в случае успеха и –1 в противном случае. При возникновении ошибки переменная errno может принимать следующее значение: EINVAL SIGNUM содержит неправильный номер сигнала. Для проверки того, обработчики каких сигналов активны в настоящий момент используется следующая функция: int sigpending(sigset_t *SET)
Функция возвращает информацию об активных в текущий момент сигналах. Если имеется заблокированный сигнал, поступивший процессу, то он также включается в маску сигналов. Возвращает 0 в случае успешного выполнения и –1 в случае ошибки. Пример проверки активных сигналов: #include <signal.h> #include <stddef.h> sigset_t base_mask, waiting_mask; sigemptyset (&base_mask); sigaddset (&base_mask, SIGINT); sigaddset (&base_mask, SIGTSTP); /* Блокировка прерываний пользователя */ sigprocmask (SIG_SETMASK, &base_mask, NULL); ... sigpending (&waiting_mask); if (sigismember (&waiting_mask, SIGINT)) { /* Попытка прекратить выполнение процесса */ } else if (sigismember (&waiting_mask, SIGTSTP)) { /* Попытка остановить выполнения процесса */ }
28
Ожидание сигнала Если программа управляется внешними событиями или использует сигналы для синхронизации, то целесообразно вместо постоянной проверки флага, указывающего на появление сигнала использовать функцию ожидания поступления сигнала. int pause()
Функция приостанавливает выполнение программы до прихода сигнала, который вызывает выполнение обработчика или прерывание выполнения процесса. Если сигнал вызывает обработчик, то функция всегда возвращает значение –1, а errno принимает следующее значение: EINTR Функция прервана приходом сигнала. В случае прекращения выполнения процесса функция не возвращает никакого значения. Функция определена в заголовочном файле . Однако, при использовании этой функции возникает серьезная проблема безопасности работы программы. pause безопасно может быть использована только в программе, которая в основном теле программы только вызывает саму функцию pause, а всю полезную работу выполняет обработчик события. Например, в следующем фрагменте программы /* `usr_interrupt' устанавливается обработчиком сигнала. if (!usr_interrupt) pause ();
/* Выполняется работа, при приходе сигнала. ...
*/
*/
Сигнал может поступить после того, как проверена переменная, но до вызова pause. Если более не поступит ни одного сигнала, программа никогда не восстановит работоспособность. Можно установить максимальное время ожидания с помощью функции sleep: while (!usr_interrupt) sleep (1); ...
Однако, и такой способ подходит не во всех случаях. Наиболее безопасным способом ожидания сигнала является использование функции sigsuspend: int sigsuspend(sigset_t *SET)
Функция заменяет маску сигналов процесса и приостанавливает его работу до поступления одного из сигналов, который не заблокирован маской. Маска, задаваемая SET действует только на время ожидания, вызываемому функцией, после возвращения управления процессу восстанавливается старая маска сигналов. Использование sigsuspend в следующем примере абсолютно безопасно: sigset_t mask, oldmask; ... /* Устанавливается маска сигналов. */ sigemptyset (&mask); sigaddset (&mask, SIGUSR1); ... /* Ожидается приход сигналов. */ sigprocmask (SIG_BLOCK, &mask, &oldmask); while (!usr_interrupt) sigsuspend (&oldmask); sigprocmask (SIG_UNBLOCK, &mask, NULL);
29
Использование отдельного стека сигнала Стек сигнала – это специальный участок памяти, используемый как стек во время вызова обработчика сигнала. Размер стека должен быть достаточно большим для того, чтобы обезопаситься от переполнения. Стандартный размер стека сигнала для Linux задается макросом SIGSTKSZ. Для выделения пространства может использоваться функция malloc. Для описания стека используется следующая структура sigaltstack. Она включает следующие поля: void *ss_sp указатель на область данных стека; size_t ss_size размер в байтах стека, на который указывает ss_sp. В Linux определены два макроса, которые могут использоваться для установки размера стека: SIGSTKSZ – стандартное значение для стека сигнала, достаточное для большинства операций; MINSIGSTKSZ – минимальное значение размера, необходимое операционной системе для вызова обработчика. int ss_flags поле может содержать битовую маску следующих значений: SS_DISABLE – система не должна использовать стек сигнала; SS_ONSTACK – устанавливается системой и показывает, что в настоящий момент используется. int sigaltstack(const struct sigaltstack *STACK, const struct sigaltstack *OLDSTACK)
Функция указывает операционной системе на необходимость использования альтернативного стека. Если OLDSTACK не является нулевым указателем, в этом аргумент заносится информация о текущем стеке. Возвращает значение 0, если функция выполнена успешно, -1 – в случае ошибки. В случае ошибки переменная errno может принимать следующие значения: EINVAL была попытка отключит стек, который в настоящее время используется; ENOMEM размер альтернативного стека недостаточен для работы.
30
Библиотека GDBM GDBM – библиотека функций для работы с хешированной базой данных. Основная сфера применения GDBM – хранение пар ключ/данные в файле данных. Каждый ключ должен быть уникальным, каждому ключу должен соответствовать только одно значение данных. Ключи не могут быть считаны в сортированном порядке. Модули данных, хранимые в базе данных, задаются с помощью следующей структуры: typedef struct { char *dptr; int dsize; } datum;
Структура позволяет использовать ключи и данные произвольного размера. Пары ключ/данные хранятся в дисковом файле, называемом базой данных gdbm. Приложение должно открыть базу данных прежде чем начать работать с ключами и данными, включенными в базу данных. Библиотека позволяет приложению открывать несколько баз данных в один момент времени. Когда приложение открывает базу данных с правами чтения или записи. В каждый момент времени база данных может быть открыта только одним приложением, у которого имеются права на запись или несколькими приложениями с правами на чтение. Открыть одновременно базу данных различными приложениями на чтение и запись невозможно. Все функции библиотеки определены в заголовочном файле . Открытие базы данных GDBM_FILE gdbm_open(char *name, int block_size, int flags, int mode, void (*fatal_func)())
Функция инициализирует систему gdbm. Если файл имеет нулевой размер, выполняется процедура инициализации файла, устанавливающая начальную структуру файла. При вызове задаются следующие параметры: name Имя файла базы данных. block_size Используется при инициализации базы данных и задает размер блока обмена между оперативной памятью и диском. Если задаваемое значение меньше 512, то используется стандартное значение, иначе – задаваемое программой. flags Поле может принимать следующие значения: - GDBM_READER – доступ к существующей базе данных в режиме чтения; - GDBM_WRITER – доступ к существующей базе данных в режиме чтения и записи; - GDBM_WRCREAT – если база не существует, создается новая, пользователь получает доступ в режиме записи и чтения; - GDBM_NEWDB – создается новая запись независимо от того существует ли файл базы данных или нет, пользователь получает доступ в режиме чтения и записи. К флагам GDBM_WRITER, GDBM_WRCREAT, GDBM_NEWDB может быть добавлен флаг GDBM_FAST, которая указывает, что необходимо записывать данные в базу данных без синхронизации с файлом на диске. Это увеличивает быстродействие, однако может привести к неправильным данным в файле в случае возникновения фатальной ошибки. mode Режим открытия файла. Идентичен режимам, задаваемым функциями open и chmod.
31
fatal_func
Функция, которая будет вызвана в случае возникновения фатальной ошибки. Если аргумент является нулевым указателем, вызывается функция по умолчанию. Функция возвращает указатель, используемый остальными функциями для обращения к данным. Если возвращаемый указатель равен нулю, произошла ошибка. Закрытие базы данных Закрытие базы данных выполняется функцией gdbm_close(GDBM_FILE dbf)
dbf – указатель, возвращаемый при открытии базы данных. Функция закрывает файл базы данных и освобождает всю задействованную память при работе с базой данных. Добавление и обновление записей в файле баз данных Функция gdbm_store добавляет или обновляет записи в файле базы данных: int gdbm_store(GDBM_FILE dbf, datum key, datum content, int flag)
dbf key content flag
-1 1 0
Функции передаются следующие параметры: Указатель, возвращаемый при открытии базы данных. Данные ключа. Данные, ассоциированные с ключом. Определяет действия, которые выполняются если запись с заданным ключом уже существует. Если аргумент равен GDBM_REPLACE, то данные замещаются новым значение content. Если аргумент равен GDBM_INSERT, то будет возвращен код ошибки и никаких действий выполнено не будет. Функция возвращает следующие значения: Данные не были сохранены в базе данных так как база данных не была открыта в режиме чтения или в аргументах key или content поле dptr равно нулю. Данные не были сохранены в базе данных, так как flag имеет значение GDBM_INSERT запись с заданным ключом уже существует. Функция выполнена успешно. Поиск записей в базе данных Для поиска в выборки данных с заданным ключом служит функция:
datum gdbm_fetch(GDBM_FILE dbf, datum key)
Функции передаются следующие параметры: dbf Указатель, возвращаемый при открытия базы данных. key Данные ключа. Возвращаемое значение – указатель на найденные данные. Если поле dptr равно нулю, данные не найдены. Функция выделяет участок памяти для хранения найденных данных, однако не освобождает его автоматически. int gdbm_exists(GDBM_FILE dbf, datum key)
Эта функция не выделяет динамическую память, а лишь возвращает значение 1 в случае, если запись найдена и 0 в противном случае. Удаление записей из базы данных Для удаления записей из базы данных служит следующая функция:
int gdbm_delete(GDBM_FILE dbf, datum key)
32
Функция возвращает –1, если запись не существует или база данных открыта только для чтения. Последовательный доступ к данным Следующие две функции позволяют получить доступ ко всем записям базы данных. Функция gdbm_firstkey выдает первую запись базы данных, функция gdbm_nextkey выдает следующую запись. datum gdbm_firstkey(GDBM_FILE dbf) datum gdbm_nextkey(GDBM_FILE dbf, datum key)
Функциям передаются следующие параметры: dbf Указатель, возвращаемый при открытии базы данных. key Данные ключа. Возвращаемое значение функций – данные ключа найденной записи. Если поле dptr структуры данных ключа равно нулю, искомая запись не существует. Функции выделяют для хранения возвращаемого значения область памяти, которая не освобождается автоматически. Порядок просмотра записей файла зависит от хэш-таблицы. При выполнении каждой операции удаления записи, хэш-таблица пересматривается и порядок просмотра записей может изменяться. Реорганизация базы данных int gdbm_reorganize(GDBM_FILE dbf)
Эта функция должна выполняться очень редко. Если было выполнено много операций удаления записей и есть необходимость сокращения пространства, занимаемого базой данных, эта функция реорганизует базу данных. Другими способами размер базы данных не сокращается. Для реорганизации создается новый файл, в который вставляю все записи старого файла, после чего новый файл переименовывается с именем старого. При большом объеме данных операция может занять довольно продолжительное время. Функция возвращает отрицательное значение, если произошла ошибка и 0 в противном случае. Синхронизация базы данных Если база данных была открыта с флагом GDBM_FAST, функции библиотеки не ожидают записи на диск перед возвращением управления в программу. Это обеспечивает более быстрый доступ к базе данных. Следующая функция позволяет убедиться в том, что дисковая версия базы данных полностью обновлена: void gdbm_sync(GDBM_FILE dbf)
Функция обычно вызывается после окончания некоторого множества изменений в базе данных. Функция gdbm_close автоматически вызывает процедуру синхронизации, поэтому перед закрытием базы данных в непосредственном вызове функции синхронизации нет необходимости. Сообщения об ошибках Каждая функция библиотеки возвращает код возникшей ошибки в глобальной переменной errno. Для выдачи текстового сообщения об ошибке, соответствующего этому коду может быть вызвана следующая функция: char *gdbm_strerror(gdbm_error errno)
33
Установка параметров Библиотека поддерживает возможность установки параметров для открытой базы данных. Для этого используется функция int gdbm_setopt(GDBM_FILE dbf, int option, int *value, int size)
Функции передаются следующие параметры: Указатель, возвращаемый при открытии базы данных. Устанавливаемая опция. Могут быть установлены следующие параметры: - GDBM_CACHESIZE – размер выделяемого пространства под кэш. По умолчанию устанавливается равным 100; - GDBM_FASTMODE – включает или выключает “быстрый” режим. Допустимыми значениями являются TRUE и FASLE. value Значение, которое будет установлено для параметра. size Размер данных, передаваемых в value. Возвращает –1 в случае ошибки и 0 в противном случае. Например, для установке для базы данных кэша размером 10, но до ее использования можно выполнить следующее:
dbf option
int value = 10; ret = gdbm_setopt(dbf, GDBM_CACHESIZE, &value, sizeof(int));
34
Каналы и FIFO-файлы Канал – это один из механизмов UNIX, предназначенный для связи между процессами. Данные, записанные в канал одним процессом могут быть считаны другим процессом. Данные поступают в канал и выдаются из него в порядке “первым пришел, первым вышел”. Канал не имеет имени и создается для однократного использования. FIFO-файлы схожи с каналами, но в отличие от каналов они не являются временными соединениями между процессами и имеют одно или несколько имен как у любого файла операционной системы. Процессы открывают FIFO-файл используя его имя. Каналы и FIFO-файлы должны быть открыты двумя взаимодействующими процессами независимо. При чтении из канала или FIFO-файла, в который ни один поток не записывает данные (например, если файл или канал уже был закрыт), операция чтения возвращает символ конца файла. Запись в канал, не имеющий ни одного процесса, выполняющего операцию чтения трактуется как ошибка и генерирует сигнал SIGPIPE и завершается с кодом ошибки, равным EPIPE, если сигнал обрабатывается или заблокирован. Ни каналы ни FIFO-файлы не имеют механизма позиционирования указателя. И чтения и запись осуществляются последовательно. Чтение выполняется из начала файла, запись – в конец файла. Создание канала Для создания канала служит функция pipe. Она используется для открытия канала и на чтение и на запись. Обычно процесс создает канал до создания порожденных процессов, с которыми он будет взаимодействовать через канал. Канал может использоваться для взаимодействия как между родительским и порожденным процессами, так и между двумя равноправными процессами. Функция pipe определена в заголовочном файле : int pipe(int FILEDES[2])
Функция создает поток и помещает дескрипторы файлов для чтения из потока и записи в поток соответственно в FILEDES[0] и FILEDES[1]. В случае успешного выполнения функция возвращает значение 0, при возникновении ошибки возвращается –1, в переменная errno может принимать следующие значения: EMFILE процесс имеет слишком много открытых файлов; ENFILE система имеет слишком много открытых файлов. В Linux эта ошибка никогда не возникает. Приведем пример создания канала. Программа использует функцию fork для создания порожденного процесса. Родительский процесс записывает данные в канал, которые считывает порожденный процесс. #include #include #include #include
<sys/types.h> <stdio.h> <stdlib.h>
void read_from_pipe (int file) { FILE *stream; int c; stream = fdopen (file, "r"); while ((c = fgetc (stream)) != EOF) putchar (c); fclose (stream); }
35
void write_to_pipe (int file) { FILE *stream; stream = fdopen (file, "w"); fprintf (stream, "hello, world!\n"); fprintf (stream, "goodbye, world!\n"); fclose (stream); } int main (void) { pid_t pid; int mypipe[2]; if (pipe (mypipe)) { fprintf (stderr, "Pipe failed.\n"); return EXIT_FAILURE; }
}
pid = fork (); if (pid == (pid_t) 0) { read_from_pipe (mypipe[0]); return EXIT_SUCCESS; } else if (pid < (pid_t) 0) { fprintf (stderr, "Fork failed.\n"); return EXIT_FAILURE; } else { write_to_pipe (mypipe[1]); return EXIT_SUCCESS; }
Взаимодействие с порожденным процессом Обычное использование каналов заключается в том, чтобы посылать или получать данные от порожденного процесса. Один из способов выполнения этой задачи заключается в использовании функций pipe(для создания канала), fork (для создания порожденного процесса), dup2 (для того, чтобы “заставить” порожденный процесс использовать канал как выходной поток), exec(для выполнения новой программы). Однако, существует и более простой способ такого взаимодействия с использованием функций popen и pclose. FILE *popen(const char *COMMAND, const char *MODE)
Функция выполняет команду COMMAND как независимый процесс, создает канал, возвращает поток, ассоциированный с этим каналом и возвращает управление в вызывающую программу. Таким образом, родительский и порожденный процесс выполняются независимо. Если аргумент MODE задается как “r”, то родительский процесс может получить данные из стандартного канала вывода порожденного процесса. Порожденный процесс наследует канал ввода от родительского процесса. Если аргумент MODE задается как “w”, то родительский процесс может послать данные в стандартный канал ввода порожденного процесса. Порожденный процесс наследует канал вывода от родительского процесса. 36
В случае возникновения ошибки функция возвращает нулевой указатель. Это может случиться в следующих случаях: если канал или поток не могут быть созданы, если не может быть создан порожденный процесс или если программа не может быть выполнена. int pclose(FILE *STREAM)
Функция используется для закрытия потока, созданного функцией popen. Она ожидает, пока не завершится порожденный поток и возвращает его статус, аналогичный возвращаемому функцией system. Приведем пример использования popen и pclose для фильтрации вывода через другую программу (в данном случае – more). #include <stdio.h> #include <stdlib.h> void write_data (FILE * stream) { int i; for (i = 0; i < 100; i++) fprintf (stream, "%d\n", i); if (ferror (stream)) { fprintf (stderr, "Output to stream failed.\n"); exit (EXIT_FAILURE); } } int main (void) { FILE *output;
}
output = popen ("more", "w"); if (!output) { fprintf (stderr, "Could not run more.\n"); return EXIT_FAILURE; } write_data (output); pclose (output); return EXIT_SUCCESS;
FIFO-файлы FIFO-файлы схожи с каналами, однако создаются другим способом. FIFOфайлы создаются в файловой системе с помощью функции mkfifo. После создания такого файла, любой процесс может открыть его для чтения и записи таким же способом, как и обычный файл. Тем не менее, он должен быть открыт и записывающим и читающим процессом прежде, чем можно будет записывать и читать из него данные. Функция mkfifo определена в заголовочном файле <sys/stat.h>: int mkfifo(const char *FILENAME, mode_t MODE)
Функция mkfifo создает FIFO-файл с именем FILENAME. Аргумент MODE задает маску доступа к файлу, идентичную используемой при создании обычного файла. При успешном выполнении функции функция возвращает 0, в случае ошибки возвращается –1, а переменная errno в кроме ошибок, связанных с именем файла, может принимать следующие значения:
37
EEXIST ENOSPC EROFS
38
файл с заданным именем уже существует; директорий или файловая система не могут быть расширены; попытка создания файла в файловой системе “только для чтения”.
Гнезда Гнездо является обобщенным межпроцессным каналом взаимодействия. Как и каналы, гнезда представляются файловым дескриптором, но в отличие от каналов поддерживают взаимодействие между несвязанными процессами и даже между процессами, работающими на различных машинах, связанных в вычислительную сеть. Основная цель использования гнезд – взаимодействие между различными компьютерами. Такие распространенные программы, как telnet, rlogin, ftp, talk используют гнезда. Основные концепции использования гнезд При создании гнезда необходимо указать способ взаимодействия, который будет использоваться, и тип протокола, обеспечивающего это взаимодействие. Способ взаимодействия гнезда определяет семантику уровня пользователя передачи данных через гнездо. Выбор способа взаимодействия определяется ответами на следующие вопросы: • Каков размер передаваемых блоков данных? Некоторые способы взаимодействия рассматривают передаваемые данные как последовательность битов не большую определенного размера, другие группируют байты в записи или пакеты; • Могут ли данные быть потеряны при выполнении операции? Некоторые способы взаимодействия гарантируют, что все посланные данные достигают пункта назначения. Другие способы могут рассматривать потерю данных как нормальное завершение операции. В этом случае один и тот же пакет данных может передаваться более одного раза или доходить до получателя в неправильном порядке; • Выполняется ли взаимодействие только с одним удаленным гнездом или одновременно с несколькими? Необходимо также выбрать так называемое “пространство имен” для именования гнезда. Имя гнезда (адрес) означает что-то конкретное только в контексте выбранного пространства имен. От выбранного пространства имен зависит и тип данных, которым представляется имя гнезда. Каждое пространство имен имеет соответствующее символьное имя, которое начинается с символов ‘PF_’. Соответствующее ему символьное имя, начинающееся с ‘AF_’ определяет формат адреса, связанный с пространством имен. Последнее, что необходимо выбрать – протокол, по которому выполняется взаимодействие. Протокол определяет то, какой низкоуровневый механизм используется для передачи и получения данных. Каждый протокол соответствует выбранному способу взаимодействия и пространству имен. Пространство имен иногда называется семейством протоколов. Правила, по которым работает протокол, определяют формат данных, которые передаются между двумя программами, расположенными, возможно, на различных компьютерах. Большая часть правил скрывается операционной системой и в их знании нет необходимости. О протоколах необходимо знать следующее: 1. для того, чтобы обмениваться данными две программы должны работать с одним и тем же протоколом; 2. каждый протокол имеет дело только с конкретным способом взаимодействия и пространством имен. Например, протокол TCP работает только с байтовым потоком данных и пространством имен Интернет; 39
3.
для каждой комбинации способа и пространства имен существует протокол по умолчанию, который можно запросить, указав 0 в качестве номера протокола.
Способы взаимодействия Стандартная библиотека Linux включает поддержку нескольких типов гнезд, обладающих различными характеристиками. Опишем поддерживаемые типы гнезд. Символьные константы, используемые для обозначения типов перечислены в заголовочном файле <sys/socket.h>. int SOCK_STREAM этот способ аналогичен каналам, он работает с удаленным гнездом и передает данные в виде потока байтов; int SOCK_DGRAM этот способ предназначен для индивидуально-адресуемых пакетов, однако пакет может не достигнуть адресата. Каждая запись некоторого набора данных формирует пакет. Так как гнезда такого типа не имеют постоянного соединения, адрес нужно указывать для каждого передаваемого пакета. Единственное, что гарантируется при использовании такого типа гнезд – попытка операционной системы отправить те пакеты, которые посылаются клиентской программой. Некоторые пакеты могут быть не доставлены, очередность доставки пакетов может быть нарушена. Обычный случай использования таких пакетов – приложения, где возможна повторная попытка в случае, если подтверждение получения не пришло в разумное время и приложения, где скорость передачи данных важнее надежности; int SOCK_RAW этот способ дает доступ к низкоуровневым сетевым протоколам и интерфейсам. Обычным пользовательским программам нет необходимости в использовании такого типа гнезд. Адреса гнезд Имя гнезда обычно называется адресом. Гнездо, созданное с помощью функции socket, не имеет адреса. Другие процессы могут найти его для взаимодействия если только идентифицировать с этим гнездом адрес. Эта операция называется “привязкой” адреса и выполняется она функцией bind. Формат адреса гнезда зависит от используемого пространства имен. Независимо от пространства имен для установки и определения адреса гнезда используются функции bind и getsockname. Эти функции используют общую структуру sockaddr для представления указателя на адрес гнезда. Однако, используя эту структуру невозможно эффективно интерпретировать или сконструировать адрес из-за того, что адреса различных пространств имен имеют различные типы данных. Наиболее распространенным способом является формирование адреса в переменной, имеющей подходящий тип данных, а затем преобразовать указатель на переменную в указатель ‘struct sockaddr *’. Тем не менее, из типа данных sockaddr можно получить идентификатор формата адреса, который будет использоваться в данном пространстве имен. Структура sockaddr определена следующим образом: struct sockaddr { short int sa_family; char sa_data[14];
40
};
sa_family sa_data
код формата адреса; указывает адрес гнезда, который зависит от формата. Его длина зависит от формата и может быть более определенной длины в 14 байтов. Формат адреса может задаваться следующими символьными именами. Каждому имени формата соответствует символьное имя пространства имен, начинающееся с символов ‘PF_’: AF_LOCAL определяет формат адреса локального пространства имен, соответствующее имя пространства имен – PF_LOCAL; AF_UNIX синоним для AF_LOCAL, соответствующее имя пространства имен – PF_UNIX; AF_FILE еще один синоним для AF_LOCAL, соответствующее имя пространства имен – PF_FILE; AF_INET определяет формат адреса для пространства имен Интернет, определяемого именем PF_INET; AF_INET6 определяет формат, идентичный AF_INET, однако соответствует протоколу IPv6, соответствующее имя пространства имен – PF_INET6; AF_UNSPEC определяет отсутствие конкретного формата адреса. Используется в редких случаях, когда необходимо очистить адрес назначения по умолчанию при отправлении дейтаграмм. Соответствующее имя пространства имен PF_UNSPEC существует, но никаких причин использования его в программе не существует. В Linux определяются и многие другие типы сетей, большинство из которых в настоящее время не реализованы. Установка адреса гнезда int bind (int SOCKET, struct sockaddr *ADDR, socklen_t LENGTH)
Функция назначает адрес гнезду SOCKET. Аргументы ADDR и LENGTH определяют адрес. Первая часть адреса всегда определяет формат адреса. Функция возвращает 0 в случае успеха и –1 в случае возникновения ошибки, а переменная errno может принимать следующие значения: EBADF аргумент SOCKET не является правильным дескриптором файла; ENOTSOCK дескриптор SOCKET не является гнездом; EADDRNOTAVAIL указанный адрес не доступен на данной машине; EADDRINUSE другое гнездо уже использует указанный адрес; EINVAL гнездо уже имеет адрес; EACCES нет доступа для установки адреса гнезда. Существуют и другие коды ошибок, зависящие от используемого пространства имен. Чтение адреса гнезда Для получения адреса гнезда служит функция getsockname. Прототип функции находится в заголовочном файле <sys/socket.h>. int getsockname *LENGTH-PTR)
(int
SOCKET,
struct
sockaddr
*ADDR,
socklen_t
Функция возвращает адрес гнезда SOCKET в участки памяти, указанные аргументами ADDR и LENGTH-PTR. Возвращаемое значение равно 0, если функция
41
выполнена успешно и –1, если произошла ошибка. В случае ошибки переменная errno может принимать следующие значения: EBADF аргумент SOCKET не является правильным дескриптором файла; ENOTSOCK дескриптор SOCKET не является гнездом; ENOBUFS размера внутренних буферов недостаточно для выполнения операции. Имена интерфейсов Каждый сетевой интерфейс имеет собственное имя. Обычно оно состоит из нескольких букв, определяющих тип интерфейса и цифры, определяющей его номер, если таких интерфейсов несколько. К таким именам интерфейсов относятся, например, lo (интерфейс обратной связи), eth0 (первый интерфейс Ethernet). Однако использование символьных имен интерфейсов в программах довольно неудобно, поэтому интерфейсы определяются выбранных случайно целым положительным числом. Следующие функции, константы и типы данных определяются в заголовочном файле . const size_t IFNAMSIZ
Эта константа определяет максимальный размер буфера, необходимого для хранения имени интерфейса, включающего завершающий нулевой символ. unsigned int if_nametoindex (const char *ifname)
Эта функция выдает индекс интерфейса, соответствующий символьному имени.
char * if_indextoname (unsigned int ifindex, char *ifname)
Эта функции соотносит индекс интерфейса с его символьным именем. Возвращаемое имя помещается в буфер, на который указывает аргумент ifname, который должен быть длиной по крайней мере IFNAMSIZE байтов. Если задаваемый индекс интерфейса неправилен, функция возвращает нулевое значение. struct if_nameindex { unsigned int if_index; char *if_name; };
Этот тип данных используется для хранения данных об интерфейсе. Здесь if_index – индекс интерфейса, if_name – символьное имя интерфейса, заканчивающееся нулевым символом. struct if_nameindex * if_nameindex (void)
Эта функция возвращает массив структур if_nameindex. Каждый элемент массива соответствует одному присутствующему в системе интерфейсу. Конец массива указывается элементом, в котором номер интерфейса равен нулю, а указатель на символьное имя является нулевым указателем. Если произошла ошибка, функция возвращает нулевой указатель. void if_freenameindex (struct if_nameindex *ptr)
Эта функция освобождает память, занимаемую структурой, создаваемой вызовом if_nameindex. Локальное пространство имен Локальное пространство имен в некоторым источникам может называться также “доменными гнездами”. В локальном пространстве имен адреса гнезд представляются именами файлов. Возможно назначить любое имя файла адресом гнезда, однако необходимо иметь права на запись в том директории, где создается такой файл. В случае подключения в гнезду,
42
необходимо иметь права на чтение этого файла. Обычно такие файлы размещаются в директории /tmp. Особенностью локального пространства имен является то, что имя файла используется только при открытии соединения. Еще одной особенностью является то, что невозможно подключиться к такого типа гнезду с другого компьютера даже если они используют разделяемую файловую систему, содержащую имя гнезда. После окончания использования гнезда в локальном пространстве имен необходимо удалить файл с помощью функций unlink или remove. Локальное пространство имен поддерживает только один протокол; этот протокол имеет номер “0”. Для создания гнезда используйте константу PF_LOCAL в качестве аргумента NAMESPACE функций socket и socketpair. Структура для описания имени гнезда определена в заголовочном файле <sys/un.h>. struct sockaddr_un { short int sun_family; char sun_path[108]; };
Здесь sun_family определяет формат адреса гнезда. Для локального пространства имен это поле должно содержать значение AF_LOCAL, sun_path – имя файла, которое должно использоваться в качестве имени гнезда. Параметр LENGTH функции bind должен был установлен как сумма поля sun_family и длины строки имени файла (не выделенного размера поля sun_path!). Для вычисления значения LENGTH может использоваться макрос SUN_LEN: int SUN_LEN (_struct sockaddr_un *_ PTR)
Приведем пример создания и именования гнезд локального пространства имен: #include #include #include #include #include #include
<stddef.h> <stdio.h> <errno.h> <stdlib.h> <sys/socket.h> <sys/un.h>
int make_named_socket (const char *filename) { struct sockaddr_un name; int sock; size_t size; sock = socket (PF_LOCAL, SOCK_DGRAM, 0); if (sock < 0) { perror ("socket"); exit (EXIT_FAILURE); } name.sun_family = AF_LOCAL; strncpy (name.sun_path, filename, sizeof (name.sun_path)); size = (offsetof (struct sockaddr_un, sun_path) + strlen (name.sun_path) + 1); if (bind (sock, (struct sockaddr *) &name, size) < 0) { perror ("bind"); exit (EXIT_FAILURE); } return sock;
43
}
Пространство имен интернет Первоначально пространство имен интернет использовало только протокол интернет (IP) версии 4 (IPv4), имеющий 32-битные адреса. С постоянным ростом количества хостов в интернет появилась необходимость в создании нового протокола с большим адресным пространством. В настоящее время Linux может использовать также и версию IPv6 протокола интернет, имеющий 128-битное адресное пространство, который должен со временем заменить IPv4. Для создания гнезда в пространстве имен IPv4 используется символьное имя PF_INET. Для создания гнезда в пространстве IPv6 используется символьное имя PF_INET6. Адрес в пространстве интернет включает следующие компоненты: • адрес машины, к которой необходимо подключиться; • номер порта этой машины. Типы данных для представления адреса гнезда в пространстве имен интернет определены в заголовочном файле . Для адреса IPv4: struct sockaddr_in { sa_family_t sin_family; struct in_addr sin_addr; unsigned short int sin_port; };
Структура данных используется для представления адресов в пространстве имен интернет стандарта IPv4. Здесь sin_family определяет формат адреса гнезда; sin_addr определяет адрес хоста в сети; sin_port – номер порта. При вызове функций bind и getsockname аргумент LENGTH нужно установить значением sizeof (struct sockaddr_in). Для представления адреса IPv6 используется структура struct sockaddr_in6 { sa_family_t sin6_family; struct in6_addr sin6_addr; uint32_t sin6_flowinfo; uint16_t sin6_port; };
Здесь sin6_family – формат адреса гнезда, sin6_addr – адрес хоста, sin6_flowinfo – в настоящий момент не используется, sin6_port – адрес порта. Адреса хостов Каждый компьютер в сети интернет имеет один или несколько адресов, номеров, которые идентифицируют компьютер среди всех остальных в сети. Обычно адреса пространства IPv4 представляется в виде четырех чисел, разделенных точками, адреса пространства IPv6 – в виде последовательность до восьми чисел, разделенных двоеточиями. Примеры адресов: IPv4 - 128.52.46.32, IPv6 - 5f03:1200:836f:c100::1. Каждый компьютер имеет также одно или более символьных имен, состоящих из слов, разделенных точками. Программы, позволяющие пользователю указать хост, с которым необходимо установить соединение, должны давать возможность указывать и символьное и числовое представление адреса. Однако, программе необходим числовой адрес для установления соединения, поэтому необходимо конвертировать символьное представление в числовое.
44
Исторически адреса IPv4 разделены на две части – номер сети и локальный адрес в сети. Все адреса стандарта IPv4 разделены на 3 класса: A, B и C. В них соответственно 1, 2 и 3 байта представляют собой адрес сети, остальная часть адреса – локальный адрес в сети. Адреса IPv4 могут быть и бесклассовыми. В этом случае адрес хоста задается в виде пары 32-битный адрес плюс 32-битная маска подсети. Маска подсети имеет вид, например 255.0.0.0. В этом случае биты, установленные в 1 указывают часть адреса, определяющую адрес сети, биты установленные в 0 – локальный адрес в сети. Бесклассовые адреса могут быть записаны и следующим образом: 10.0.0.0/8. Это означает, что на адрес сети отводится 8 бит, остальная часть адреса – локальный адрес. Адреса IPv6 всегда являются бесклассовыми. Адреса IPv4 программно представляются в некоторых случаях как целочисленное значение (тип данных uint32_t), в других случаях это значение упаковывается в структуру struct in_addr. Адреса IPv6 упаковываются в структуру struct in6_addr. Для задания интернет-адресов в заголовочном файле определены следующие типы данных и константы: struct in_addr
Этот тип данных используется в некоторых случаях для задания адресов стандарта IPv4. Структура имеет единственное поле s_addr типа uint32_t, определяющее адрес хоста. uint32_t INADDR_LOOPBACK
Эта макроподстановка может использоваться для работы с локальной машиной вместо попыток определить действительный адрес. Это адрес IPv4 127.0.0.0, который также называется localhost. В случае обращения к адресу INADDR_LOOPBACK взамен действительного адреса машины при передаче отсутствует сетевой трафик. uint32_t INADDR_ANY
Константа, представляющая “любой входящий адрес”. uint32_t INADDR_BROADCAST
Константа, представляющая широковещательный адрес. uint32_t INADDR_NONE
Эта константа возвращается некоторыми функциями для указания ошибки. struct in6_addr
Эта структура используется для хранения адреса IPv6. Она хранит 128-битные данные, доступ к которому можно получить используя несколько способов. struct in6_addr in6addr_loopback
Эта константа содержит так называемый кольцевой адрес стандарта IPv6. struct in6_addr in6addr_any
Эта константа представляет собой неспецифированный (пустой) адрес. Функции для манипуляции адресом хоста int inet_aton (const char *NAME, struct in_addr *ADDR)
Эта функция конвертирует адрес стандарта IPv4 NAME из стандартного представления, состоящего из чисел и точек, в двоичное представление и сохраняет его в структуру ADDR. Функция возвращает ненулевое значение в случае успешного выполнения и 0 в случае ошибки. uint32_t inet_addr (const char *NAME)
Функция конвертирует адрес стандарта IPv4 из стандартного представления, состоящего из чисел, разделенных точками, в двоичное представление и возвращает полученное значение. Если задан неправильный адрес, функция возвращает значение INADDR_NONE. Функция является устаревшей, так как адрес INADDR_NONE является допустимым. Рекомендуется использовать inet_addr. 45
uint32_t inet_network (const char *NAME)
Функция извлекает номер сети из передаваемого ей в параметре адреса хоста, заданного в виде чисел, разделенных точками. Если задан неправильный адрес хоста, функция возвращает –1. Функция используется только с адресами типов A, B и C и не работает с бесклассовыми адресами. char * inet_ntoa (struct in_addr ADDR)
Функция конвертирует адрес стандарта IPv4 в строку, состоящую из чисел, разделенных точками. Возвращаемое значение является указателем на статическивыделенную область памяти. Последующие вызовы функции могут перезаписать содержимое этого буфера, поэтому для дальнейшего использования возвращаемое значение необходимо скопировать. В многопотоковых программах функция создает буфер для каждого потока. Вместо этой функции рекомендуется использовать функцию inet_ntop, которая работает с адресами в обеих стандартах. struct in_addr inet_makeaddr (uint32_t NET, uint32_t LOCAL)
Эта функция создает адрес IPv4 из составляющих его номера сети и локального адреса.
uint32_t inet_lnaof (struct in_addr ADDR)
Эта функция возвращает значение локального адреса задаваемого адреса хоста. Работает только с адресами классов A, B и C. uint32_t inet_netof (struct in_addr ADDR)
Эта функция возвращает значение номера сети задаваемого адреса хоста. Работает только с адресами классов A, B и C. int inet_pton (int AF, const char *CP, void *BUF)
Эта функция конвертирует адрес интернет (как IPv4, так и IPv6) из текстового представления в двоичный формат. Аргумент AF может принимать значения AF_INET и AF_INET6 в зависимости от того, какого типа адрес необходимо конвертировать. CP – указатель на входную строку; BUF – указатель на буфер, куда будет сохранен результат операции. Необходимо позаботиться о том, чтобы размер буфера был достаточен для хранения результата. const char * inet_ntop (int AF, const void *CP, char *BUF, size_t LEN)
Функция конвертирует адрес интернет (как IPv4, так и IPv6) из двоичного формата в текстовое представление. Аргумент AF может принимать значения AF_INET и AF_INET6 в зависимости от того, какого типа адрес необходимо конвертировать. CP – указатель на адрес, который необходимо конвертировать; BUF – указатель на буфер, куда будет сохранен результат; LEN – длина этого буфера. Функция возвращает адрес буфера результата. Имена хостов Для внутреннего пользования операционная система для преобразования символьных имен хостов в числовые адреса использует свою базу данных. База данных обычно расположена в файле /etc/hosts. При отсутствии запрашиваемого символьного имени в базе данных, система обращается к сервису, называемому сервером имен, если он явно указан в настройках сети. Функции и структуры данных для доступа к этой базе данных определены в заголовочном файле . struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; char *h_addr; };
46
Эта структура данных используется для представления записи в базе данных хостов. Структура имеет следующие поля: h_name “официальное” имя хоста; h_aliases альтернативные имена хоста, определенные как вектор строк, заканчивающихся нулем; h_addrtype тип адреса хоста. Может содержать значения AF_INET и AF_INET6; h_length длина в байтах каждого адреса; h_addr_list вектор адресов хоста (хост может быть подключен к нескольким сетям, для каждой из которых он может иметь уникальный адрес). Вектор заканчивается нулевым указателем; h_addr синоним для h_addr_list[0]. Следующие функции предназначены для поиска в базе данных хостов. Результат поиска возвращается в размещенном статически блоке памяти. struct hostent * gethostbyname (const char *NAME)
Функция возвращает информацию о хосте NAME. Если такой хост не найден, возвращается нулевой указатель. struct hostent * gethostbyname2 (const char *NAME, int AF)
Эта функция идентична gethostbyname кроме того, что позволяет указать тип возвращаемого адреса. struct hostent * gethostbyaddr (const char *ADDR, int LENGTH, int FORMAT)
Возвращает информацию о хосте с интернет-адресом ADDR. В действительно ADDR может не быть указателем на char, это может быть указатель на адрес как в стандарте IPv4, так и IPv6. Аргумент LENGTH задает длину адреса, задаваемого в ADDR. FORMAT указывает формат адреса. Если хост с таким адресом не найден, функция возвращает нулевой указатель. Если поиск с использованием функций gethostbyname и gethostbyaddr завершился с ошибкой, переменная errno может принимать следующие значения: HOST_NOT_FOUND Хост не найден в базе данных. TRY_AGAIN Соединение с сервером имен не было установлено. При повторной попытке поиска функция может выполниться успешно. NO_RECOVERY Произошла фатальная ошибка. NO_ADDRESS База данных содержит запись для задаваемого имени хоста, но отсутствует соответствующий адрес. Рассмотренные выше функции не являются повторно входимыми и, соответственно, не могут использоваться в многопотоковых приложениях. В этом контексте могут использоваться следующие функции, определенные в Linux. int gethostbyname_r (const char *restrict NAME, struct hostent *restrict RESULT_BUF, char *restrict BUF, size_t BUFLEN, struct hostent **restrict RESULT, int *restrict H_ERRNOP)
Функция возвращает информацию о хосте NAME. При вызове функции необходимо передать указатель на буфер результата в параметре RESULT_BUF. Функция также может понадобиться дополнительное пространство памяти, указатель на которое и длина буфера передаются в параметрах BUF и BUFLEN. Указатель на буфер, в котором хранится результат, помещается в RESULT. Если произошла ошибка или запись не была найдена, RESULT возвращает нулевой указатель. Функция выполнилась успешно, если ее возвращаемое значение равно 0, в противном случае возвращаемое значение равно коду ошибки. В дополнение к кодам, 47
которые возвращает gethostbyname определен код ERANGE. В этом случае вызов функции должен быть повторен с большим буфером. Дополнительная информация об ошибке хранится не в глобальной переменной errno, а в структуре, на которую указывает H_ERRNOP. struct hostent *gethostname (char *host) { struct hostent hostbuf, *hp; size_t hstbuflen; char *tmphstbuf; int res; int herr; hstbuflen = 1024; tmphstbuf = malloc (hstbuflen);
}
while ((res = gethostbyname_r (host, &hostbuf, tmphstbuf, hstbuflen, &hp, &herr)) == ERANGE) { /* Enlarge the buffer. */ hstbuflen *= 2; tmphstbuf = realloc (tmphstbuf, hstbuflen); } /* Check for errors. */ if (res || hp == NULL) return NULL; return hp->h_name;
int gethostbyname2_r (const char *NAME, int AF, struct hostent *restrict RESULT_BUF, char *restrict BUF, size_t BUFLEN, struct hostent **restrict RESULT, int *restrict H_ERRNOP)
Эта функция идентична gethostbyname_r, кроме того, что позволяет указывать тип адреса в аргументе AF. int gethostbyaddr_r (const char *ADDR, int LENGTH, int FORMAT, struct hostent *restrict RESULT_BUF, char *restrict BUF, size_t BUFLEN, struct hostent **restrict RESULT, int *restrict H_ERRNOP)
Функция возвращает информацию о хосте с интернет-адресом ADDR. Параметр ADDR в действительности не является указателем на char – он может быть указателем на адрес типа IPv4 или IPv6. Аргумент LENGTH задает длину адреса в ADDR. FORMAT указывает тип адреса. Как и при вызове функции gethostbyname_r необходимо выделить буфер для результата и область памяти, используемую функцией. В случае успеха функция возвращает 0, в противном случае – код ошибки. С помощью следующих функций можно просканировать всю базу данных хостов, последовательно выбирая по одной записи. Эти функции не являются повторно входимыми. void sethostent (int STAYOPEN)
Функция открывает базу данных хостов для сканирования. Если параметр STAYOPEN не равен нулю, устанавливается флаг, который указывает, что вызовы
48
gethostbyname и gethostbyaddr не будут закрывать базу данных (что обычно происходит). struct hostent * gethostent (void)
Возвращает следующую запись из базы данных хостов. Если предыдущая запись была последней, возвращается нулевой указатель. void endhostent (void)
Эта функция закрывает базу данных. Интернет-порты Номер порта в пространстве имен определяет гнездо на конкретной машине. Порты могут иметь номера от 0 до 65535. Номера портов, меньшие, чем IPPORT_RESERVED зарезервированы для стандартных серверов таких, как finger и telnet. Существует база данных, которая хранит значения зарезервированных портов. Для получения сервисов, закрепленных за портами можно использовать функцию getservbyname. При разработке сервера, который не относится к стандартным серверам, определенным в базе данных, для него необходимо выбрать номер порта. Для этого используются порты, значения которых превышают IPPORT_USERRESERVED. Эти порты зарезервированы для серверов и не могут генерироваться операционной системой. При использовании гнезда без указания его адреса операционная система генерирует номер порта. Этот номер выбирается из диапазона IPPORT_RESERVED IPPORT_USERRESERVED. В адресном пространстве интернет допустимо использование двух различных гнезд, имеющих один и тот же номер, но только в том случае, если они не предпримут попытку взаимодействовать с тем же адресом гнезда (включающим адрес хоста и номер порта). Обычно необходимости в дублировании номера порта нет за исключением случаев, когда этого требует высокоуровневый сетевой протокол. В этом случае гнездо необходимо создавать с использованием опции SO_REUSEADDR. База данных сервисов База данных, которая хранит информацию о сервисах обычно расположена в файле ‘/etc/services’ или на сервере имен. С помощью следующих структур и функций, определенных в заголовочном файле можно получить доступ к этой базе данных. struct servent { char *s_name; char **s_aliases; int s_port; char *s_proto; };
s_name s_aliases s_port s_proto
имя сервиса; альтернативные имена сервиса. Задаются в виде массива строк. Массив завершается нулевым указателем; номер порта сервиса; имя протокола, используемого с сервисом.
struct servent * getservbyname (const char *NAME, const char *PROTO)
Функция возвращает информацию о сервисе, называющемся NAME и использующего протокол PROTO. Если сервис не найден, возвращается нулевой указатель. Функция может использоваться как для серверов, так и для клиентов; серверы могут использовать ее для определения того, какой порт следует прослушивать. 49
struct servent * getservbyport (int PORT, const char *PROTO)
Возвращает информацию о сервисе, использующем порт PORT и протокол PROTO.
void setservent (int STAYOPEN)
Функция открывает базу данных сервисов. Если аргумент STAYOPEN не равен нулю, вызовы функций getservbyname и getservbyport не будут закрывать базу данных (что обычно происходит). struct servent * getservent (void)
Возвращает следующую запись из базы данных. Если предыдущая запись была последней, возвращается нулевой указатель. void endservent (void)
Закрывает базу данных сервисов. Последовательность байтов Различные компьютеры используют разные соглашения по расположению байтов в машинном слове. В некоторых компьютерах более значащий байт расположен на первом месте, в других – наоборот. Для того, чтобы такие компьютеры могли взаимодействовать в сети, протоколы интернет определяют соглашение по последовательности байтов для данных, передаваемых в сети, которое называется “сетевым порядком байтов”. При использовании гнезд для взаимодействия приложений нужно учитывать, что поля sin_port и sin_addr структуры sockaddr_in представлены в сетевом порядке. Если в пакете данных задаются какие-либо данные целочисленного типа, их также необходимо преобразовать в сетевой порядок. Если вы используете getservbyname, getservbyport и inet_addr для получения адреса и номера порта, возвращаемые данные уже содержатся в сетевом порядке. Следующие функции используются для конвертации слов из машинного порядка в сетевой. uint16_t htons (uint16_t HOSTSHORT)
Конвертирует HOSTSHORT из машинного порядка в сетевой. uint16_t ntohs (uint16_t NETSHORT)
Конвертирует NETSHORT из сетевого порядка в машинный. uint32_t htonl (uint32_t HOSTLONG)
Конвертирует HOSTLONG из машинного порядка в сетевой. Используется для адресов IPv4. uint32_t ntohl (uint32_t NETLONG)
Конвертирует NETLONG из сетевого порядка в машинный. Используется для адресов IPv4. База данных протоколов Используемый по умолчанию коммуникационный протокол в пространстве имен интернет зависит от стиля взаимодействия. Для потокового взаимодействия используется протокол TCP, для взаимодействия с помощью датаграмм - UDP, для надежной передачи данных с помощью датаграмм - RDP. База данных протоколов обычно расположена в файле ‘/etc/protocols’. Для доступа к базе данных протоколов используются следующие структуры и функции, определенные в заголовочном файле . struct protoent { char *p_name; char **p_aliases; int p_proto; };
p_name
имя протокола;
50
p_aliases p_propo
альтернативные имена протокола; номер протокола.
struct protoent * getprotobyname (const char *NAME)
Возвращает информацию о протоколе, задаваемом именем NAME. struct protoent * getprotobynumber (int PROTOCOL)
Возвращает информацию о протоколе, задаваемом номером протокола PROTOCOL. void setprotoent (int STAYOPEN)
Открывает базу данных протокола для сканирования. Если STAYOPEN не равно нулю, вызовы функций getprotobyname и getprotobynumber не закрывают базу данных. struct protoent * getprotoent (void)
Возвращает следующую запись из базы данных протоколов. Если предыдущая запись была последней, возвращается нулевой указатель. void endprotoent (void)
Закрывает базу данных протоколов. Приведем пример создания гнезда в пространстве имен интернет. int make_socket (uint16_t port) { int sock; struct sockaddr_in name; /* Create the socket. */ sock = socket (PF_INET, SOCK_STREAM, 0); if (sock < 0) { perror ("socket"); exit (EXIT_FAILURE); } /* Give the socket a name. */ name.sin_family = AF_INET; name.sin_port = htons (port); name.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0) { perror ("bind"); exit (EXIT_FAILURE); } }
return sock;
Создание гнезда Функции работы с гнездами определены в заголовочном файле <sys/socket.h>. int socket (int NAMESPACE, int STYLE, int PROTOCOL)
Функция создает гнездо, используя стиль взаимодействия STYLE, пространство имен NAMESPACE и протокол PROTOCOL. Значение протокола может быть равно нулю, в этом случае используется значение по умолчанию. Возвращается дескриптор файла для создаваемого гнезда, если функция выполнилась успешно и -1 - в случае ошибки. переменная errno может принимать следующие значения: EPROTONOSUPPORT протокол или способ взаимодействия не поддерживаются пространством имен; EMFILE процесс имеет слишком много дескрипторов открытых файлов; 51
ENFILE
операционная система имеет слишком много дескрипторов открытых файлов; EACCESS процесс не имеет привилегий для создания гнезда с заданным способом взаимодействия и протоколом; ENOBUFS внутреннее буферное пространство операционной системы переполнено. Возвращаемый дескриптор файла поддерживает операции чтения и записи, но не поддерживает операции позиционирования. Закрытие гнезда При завершении использования гнезда, оно может быть закрыто с помощью вызова close. Если к моменту вызова передача данных еще не закончена, обычно функция ожидает окончания передачи, а затем закрывает дескриптор. Прервать передачу данных через гнездо можно прервать с помощью вызова функции int shutdown (int SOCKET, int HOW)
Функция закрывает соединение гнезда SOCKET. Аргумент HOW может принимать следующие значения: 0 останавливает получение данных на заданное гнездо. Если данные еще передаются, передача прекращается; 1 останавливает попытки передать данные; 2 останавливает и получение и передачу данных. В случае успешного выполнения функция возвращает значение 0, в противном случае возвращается -1. Переменная errno может принимать следующие значения: EBADF SOCKET не является правильным дескриптором файла; ENOTSOCK SOCKET не является гнездом; ENOTCONN нет соединения с SOCKET. Пары гнезд Пара гнезд состоит из двух соединенных (но не имеющих имен) гнезд. такая пара используется как неименованные каналы, за исключением двунаправленности передачи данных. Пара гнезд создается с помощью следующей функции. int
socketpair (int int FILEDES[2])
NAMESPACE,
int
STYLE,
int
PROTOCOL,
Функция создает пару гнезд, возвращаемых в FILEDES[0] и FILEDES[1]. Пара гнезд представляет собой дуплексный канал передачи данных, оба гнезда доступны как на чтение, так и на запись. Все аргументы соответствуют соответствующим аргументам функции socket. Пространство имен должно быть AF_LOCAL, протокол - по умолчанию (равен 0). Функция возвращает 0, если выполнилась успешно, в противном случае возвращается -1. Переменная errno может принимать одно из следующих значений: EMFILE процесс имеет слишком много открытых дескрипторов файлов; EAFNOSUPPORT задаваемое пространство имен не поддерживается; EPROTONOSUPPORT заданный протокол не поддерживается; EOPNOTSUPP заданный протокол не поддерживает создание пары гнезд.
52
Создание соединения При создании канала передачи данных клиент создает соединения, а сервер ожидает создания гнезда на стороне клиента. Для создания соединения используется следующая функция: int connect (int SOCKET, struct sockaddr *ADDR, socklen_t LENGTH)
Функция инициирует соединение гнезда с дескриптором файла SOCKET с гнездом, задаваемым аргументами ADDR и LENGTH. Обычно функция ожидает до тех пор, пока не будет получен ответ от сервера. Можно задать неблокируемый режим для гнезда SOCKET. В этом случае функция возвращает управление не ожидая ответа. Если функция выполнилась успешно, она возвращает значение 0, в противном случае возвращается -1. Переменная errno может принимать следующие значения: EBADF SOCKET не является дескриптором файла; ENOTSOCK SOCKET не является гнездом; EADDRNOTAVAIL заданный адрес не доступен на удаленной машине; EAFNOSUPPORT пространство имен, заданное в ADDR не поддерживается гнездом; EISCONN соединение для SOCKET уже установлено; ETIMEDOUT таймаут установки соединения; ECONNREFUSED сервер отказал в установлении соединения; ENETUNREACH сеть, заданная адресом ADDR недоступна с заданной машины; EADDRINUSE адрес гнезда, задаваемый ADDR уже используется; EINPROGRESS гнездо SOCKET находится в неблокируемом режиме и соединение не может быть установлено немедленно. EALREADY гнездо SOCKET не находится в неблокируемом режиме и находится в ожидании соединения. Прослушивание соединений В пространстве имен Интернет отсутствует механизм защиты, контролирующего доступ к порту; любой процесс на любой машине может подключиться к серверу. Для того, чтобы ограничить доступ к серверу, необходимо проверять адрес, с которого происходит попытка соединения или реализовать собственный протокол идентификации клиента. int listen (int SOCKET, unsigned int N)
Функция listen разрешает гнезду SOCKET принимать соединения, делая его гнездом сервера. Использование функции недопустимо для способов взаимодействия, не требующих установления соединений. Аргумент N устанавливает размер очереди ожидающих соединений. Когда очередь заполнена, попытки новых клиентов соединиться завершаются с кодом ошибки ECONNREFUSED до тех пор, пока сервер не вызовет функцию accept, принимающую соединение из очереди. Функция возвращает 0 в случае успешного выполнения и -1 в случае ошибки. При ошибке переменная errno может принимать следующие значения: EBADF SOCKET не является правильным значением дескриптора файла; ENOTSOCK SOCKET не является гнездом; EOPNOTSUPP гнездо SOCKET не поддерживает эту операцию.
53
Принятие соединений Гнездо-сервер может принимать соединение от нескольких клиентов одновременно. Это гнездо-сервер не становится частью соединения; вместо этого для каждого соединения создается собственное гнездо. int accept (int SOCKET, struct sockaddr *ADDR, socklen_t *LENGTH_PTR
Функция используется для принятия запроса на соединение на гнездо-сервер SOCKET. Функция ожидает появления запроса на соединение, если очередь запросов пуста и гнездо не установлено в неблокируемый режим. Аргументы ADDR и LENGTH-PTR используются для возвращения информации об имени клиентского гнезда, которое инициируется соединение. В случае успешного выполнения функция возвращает идентификатор файла для нового гнезда. В случае ошибки возвращается -1, а переменная errno может принимать следующие значения: EBADF SOCKET не является правильным идентификатором файла; ENOTSOCK SOCKET не является гнездом; EOPNOTSUPP SOCKET не поддерживает эту операцию; EWOULDBLOCK SOCKET установлен в неблокируемый режим, а запрашиваемых соединений в очереди в настоящий момент нет. Использование функции недопустимо для способов взаимодействия, не требующих установления соединений. Идентификация соединений int getpeername (int SOCKET, struct sockaddr *ADDR, socklen_t *LENGTH-PTR)
Функция возвращает адрес гнезда, которое пытается подключиться к гнезду SOCKET. Информация сохраняется в участках памяти, на которые указывают параметры ADDR и LENGTH-PTR. Функция возвращает 0 в случае успешного выполнения и -1 в случае неудачи. Переменная errno может принимать следующие значения: EBADF SOCKET не является верным дескриптором файла; ENOTSOCK SOCKET не является гнездом; ENOTCONN соединение с SOCKET не установлено; ENOBUFS недостаточно внутренних буферов. Передача данных Как только соединение между двумя гнездами установлено, можно использовать обычные операции read и write для передачи данных. Гнездо является двусторонним каналом передачи данных, поэтому операции чтения и записи могут выполняться на обоих концах соединения. Кроме того, для гнезд имеются несколько дополнительных режимов передачи данных. Эти режимы можно использовать с помощью функций recv и send. int send (int SOCKET, void *BUFFER, size_t SIZE, int FLAGS)
Функция send определена в заголовочном файле <sys/socket.h>. Если аргумент FLAGS установлен в 0, действия функции идентичны write. Если соединение было установлено, а затем разрушено, в результате выполнения функций send и write генерируется сигнал SIGPIPE. Функция возвращает количество считанных байтов или -1 в случае ошибки. Если гнездо находится в неблокируемом режиме, функция может вернуть только часть передаваемых данных. Переменная errno может принимать следующие значения:
54
EBADF EINTR ENOTSOCK EMSGSIZE EWOULDBLOCK ENOBUFS ENOTCONN EPIPE
SOCKET не является дескриптором файла; операция была прервана каким-либо сигналом до начала передачи данных; SOCKET не является гнездом; тип гнезда требует, чтобы данные передавались побайтно, а размер переданных данных превышает допустимый размер; для гнезда задан неблокируемый режим и операция записи будет заблокирована; недостаточен размер внутреннего буферного пространства; не установлено соединение с гнездом; Соединение было установлено, но в настоящий момент нарушено. В этом случае функция генерирует сигнал SIGPIPE. Однако, если этот сигнал игнорируется или заблокирован, функция завершается с кодом ошибки EPIPE.
int recv (int SOCKET, void *BUFFER, size_t SIZE, int FLAGS)
Функция recv идентична функции read, однако позволяет задать дополнительные параметры передачи в аргументе FLAGS. Если гнездо находится в неблокируемом режиме и отсутствуют данные для чтения, функция завершается с ошибкой, иначе ожидается поступление данных. Функция возвращает количество считанных байтов или -1 в случае ошибки. Переменная errno может принимать следующие значения: EBADF SOCKET не является дескриптором файла; ENOTSOCK SOCKET не является гнездом; EWOULDBLOCK гнездо находится в неблокируемом режиме и операция чтения будет заблокирована; EINTR операция была прервана сигналом до того, как были считаны какие-либо данные; ENOTCONN соединение с гнездом не установлено. Параметры передачи данных Аргумент FLAGS функций recv и send является битовой маской и может быть комбинацией следующих значений: int MSG_OOB передача или получение экстренных данных; int MSG_PEEK данные читаются или посылаются программой, но не удаляются из очереди; может использоваться только с функциями чтения; int MSG_DONTROUTE не включать в сообщение информацию о маршруте; используется только с функция посылки данных. Приведем пример использования гнезда, передающего байтовый поток данных. Пример клиента: #include #include #include #include #include #include #include #include
<stdio.h> <errno.h> <stdlib.h> <sys/types.h> <sys/socket.h>
#define PORT #define MESSAGE #define SERVERHOST
5555 "Yow!!! Are we having fun yet?!?" "mescaline.gnu.org"
55
void write_to_server (int filedes) { int nbytes;
}
nbytes = write (filedes, MESSAGE, strlen (MESSAGE) + 1); if (nbytes < 0) { perror ("write"); exit (EXIT_FAILURE); }
int main (void) { extern void init_sockaddr (struct sockaddr_in *name, const char *hostname, uint16_t port); int sock; struct sockaddr_in servername; /* Create the socket. */ sock = socket (PF_INET, SOCK_STREAM, 0); if (sock < 0) { perror ("socket (client)"); exit (EXIT_FAILURE); } /* Connect to the server. */ init_sockaddr (&servername, SERVERHOST, PORT); if (0 > connect (sock, (struct sockaddr *) &servername, sizeof (servername))) { perror ("connect (client)"); exit (EXIT_FAILURE); }
}
/* Send data to the server. */ write_to_server (sock); close (sock); exit (EXIT_SUCCESS);
Приложение сервера является несколько более сложным. Так как необходимо предоставить возможность нескольким клиентам подключиться к гнезду сервера, будет некорректным просто ожидать данных от одного клиента с помощью функций read или recv. Правильным является использование функции select для ожидания ввода от всех открытых гнезд. В примере используется функция make_socket, определенная в примере к базе данных протоколов. #include #include #include #include #include #include #include #include
<stdio.h> <errno.h> <stdlib.h> <sys/types.h> <sys/socket.h>
#define PORT #define MAXMSG
5555 512
int read_from_client (int filedes) {
56
char buffer[MAXMSG]; int nbytes;
}
nbytes = read (filedes, buffer, MAXMSG); if (nbytes < 0) { /* Read error. */ perror ("read"); exit (EXIT_FAILURE); } else if (nbytes == 0) /* End-of-file. */ return -1; else { /* Data read. */ fprintf (stderr, "Server: got message: `%s'\n", buffer); return 0; }
int main (void) { extern int make_socket (uint16_t port); int sock; fd_set active_fd_set, read_fd_set; int i; struct sockaddr_in clientname; size_t size; /* Create the socket and set it up to accept connections. */ sock = make_socket (PORT); if (listen (sock, 1) < 0) { perror ("listen"); exit (EXIT_FAILURE); } /* Initialize the set of active sockets. */ FD_ZERO (&active_fd_set); FD_SET (sock, &active_fd_set); while (1) { read_fd_set = active_fd_set; if (select (FD_SETSIZE, &read_fd_set, NULL, NULL, NULL) < 0) { perror ("select"); exit (EXIT_FAILURE); } for (i = 0; i < FD_SETSIZE; ++i) if (FD_ISSET (i, &read_fd_set)) { if (i == sock) { int new; size = sizeof (clientname); new = accept (sock, (struct sockaddr *) &clientname, &size); if (new < 0) { perror ("accept");
57
exit (EXIT_FAILURE); } fprintf (stderr, "Server: connect from host %s, port %hd.\n", inet_ntoa (clientname.sin_addr), ntohs (clientname.sin_port)); FD_SET (new, &active_fd_set);
}
}
}
} else { if (read_from_client (i) < 0) { close (i); FD_CLR (i, &active_fd_set); } }
Экстренные данные Потоки экстренных данных (out-of-band data) передаются с большим приоритетом, чем обычные данные. Обычно причина передачи экстренных данных передача сообщений об исключительных ситуациях. Как чтения, так и для отправки экстренных данных функции recv и send должны быть выполнены с установленным флагом в значение MSG_OOB. Обычные операции передачи и чтения данных не принимают и не посылают экстренные данные. Когда гнездо определяет, что поступили экстренные данные, оно генерирует сигнал SIGURG процессу-владельцу гнезда или группе процессов гнезда (в случае многопоточного приложения). Владельца гнезда можно установить используя команду F_SETOWN функции fcntl. Возможно также определить обработчик сигнала. Еще один способ определения момента поступления экстренных данных использование функции select. Передача данных с помощью дейтаграмм Пакеты данных, передаваемые с помощью дейтаграмм, независимы друг от друга, порядок получения пакетов может отличаться от порядка отправки. Пакет данных может не дойти до пункта назначения вовсе. Дейтаграммы не используют для отправки данных соединения. Функции accept и listen не используются для передачи дейтаграмм. Обычный способ передачи дейтаграммы - использование функции sendto. int sendto (int SOCKET, void *BUFFER. size_t SIZE, int FLAGS, struct sockaddr *ADDR, socklen_t LENGTH)
Функция передает данные из буфера BUFFER через гнездо SOCKET на адрес получателя, задаваемые аргументами ADDR и LENGTH. Аргумент SIZE задает размер передаваемых данных. Возвращаемое значение и коды ошибок аналогичны функции send. Однако не стоит полагаться на систему при определении ошибок передачи данных. Наиболее распространенной ошибкой является потеря пакета данных. Операционная система не может определить ошибки, связанные с передачей данных. Функция recvfrom считывает пакет дейтаграмм из гнезда и сообщает адрес отправителя. int recvfrom (int SOCKET, void *BUFFER, size_t SIZE, int FLAGS, struct sockaddr *ADDR, socklen_t *LENGTH-PTR)
58
BUFFER указывает область памяти, куда будут записаны считанные данные, SIZE задает размер используемого буфера. Если пакет имеет размер, больший SIZE, принимаются SIZE байтов, а окончание пакета теряется. Поэтому при использовании дейтаграмм необходимо заранее знать размер передаваемых пакетов данных. Аргументы ADDR и LENGTH-PTR указывают используются для возвращения адреса отправителя. Если эта информация не нужна, ADDR может быть нулевым указателем. Возвращаемое значение и коды ошибок идентичны функции recv. Для получения дейтаграмм могут использоваться также и функции recv и read. Приведем примеры сервера и клиента, работающих через механизм дейтаграмм. Сервер ожидает прихода сообщения от клиента, а затем передает это сообщение обратно. В примере используется функция make_named_socket, описанная в разделе создания гнезд в локальном пространстве имен. #include #include #include #include #include
<stdio.h> <errno.h> <stdlib.h> <sys/socket.h> <sys/un.h>
#define SERVER #define MAXMSG
"/tmp/serversocket" 512
int main (void) { extern int make_named_socket (const char *name); int sock; char message[MAXMSG]; struct sockaddr_un name; size_t size; int nbytes; /* Remove the filename first, it's ok if the call fails */ unlink (SERVER); /* Make the socket, then loop endlessly. */ sock = make_named_socket (SERVER); while (1) { /* Wait for a datagram. */ size = sizeof (name); nbytes = recvfrom (sock, message, MAXMSG, 0, (struct sockaddr *) & name, &size); if (nbytes < 0) { perror ("recfrom (server)"); exit (EXIT_FAILURE); } /* Give a diagnostic message. */ fprintf (stderr, "Server: got message: %s\n", message); /* Bounce the message back to the sender. */ nbytes = sendto (sock, message, nbytes, 0, (struct sockaddr *) & name, size); if (nbytes < 0) { perror ("sendto (server)"); exit (EXIT_FAILURE);
59
}
}
}
Клиент посылает дейтаграмму серверу, а затем ожидает ответ. #include #include #include #include #include #include #define #define #define #define
<stdio.h> <errno.h> <stdlib.h> <sys/socket.h> <sys/un.h>
SERVER CLIENT MAXMSG MESSAGE
"/tmp/serversocket" "/tmp/mysocket" 512 "Yow!!! Are we having fun yet?!?"
int main (void) { extern int make_named_socket (const char *name); int sock; char message[MAXMSG]; struct sockaddr_un name; size_t size; int nbytes; /* Make the socket. */ sock = make_named_socket (CLIENT); /* Initialize the server socket address. */ name.sun_family = AF_LOCAL; strcpy (name.sun_path, SERVER); size = strlen (name.sun_path) + sizeof (name.sun_family); /* Send the datagram. */ nbytes = sendto (sock, MESSAGE, strlen (MESSAGE) + 1, 0, (struct sockaddr *) & name, size); if (nbytes < 0) { perror ("sendto (client)"); exit (EXIT_FAILURE); } /* Wait for a reply. */ nbytes = recvfrom (sock, message, MAXMSG, 0, NULL, 0); if (nbytes < 0) { perror ("recfrom (client)"); exit (EXIT_FAILURE); } /* Print a diagnostic message. */ fprintf (stderr, "Client: got message: %s\n", message);
}
/* Clean up. */ remove (CLIENT); close (sock);
Демон inetd Существует еще один способ привязать какой-либо сервис к интернет-порту. Для этого используется программа inetd. Она активизируется в момент запуска системы и ожидает (с помощью функции select) сообщений на заданном наборе портов. При
60
получении сообщения она принимает соединение и запускает соответствующее серверное приложение. Порты и соответствующие программы могут быть заданы в файле ‘etc/inetd.conf’. При каждом запросе на соединение, запускается новый серверный процесс. В момент запуска соединение уже существует, гнездо задается с помощью стандартных входного и выходного дескрипторов файлов, которые процесс немедленно может использовать. Конфигурирование inetd Файл /etc/inetd.conf задает какие порты будут прослушиваться и какие программы будут запускаться. Обычно каждая запись в этом файле занимает одну строку. Приведем пример описания порта: ftp stream talk dgram
1 2, 3
4
5 6, 7
tcp udp
nowait wait
root root
/libexec/ftpd /libexec/talkd
ftpd talkd
По порядку поля каждой записи имеют следующие значения: указывает какой сервис обеспечивается данной программой; поля указывают стиль взаимодействия и протокол, который используется прослушивающим гнездом. Второе поле должно быть именем способа взаимодействия, переведенным в нижний регистр и без приставки SOCK_. Третье поле должно быть именем одного из протоколов, перечисленных в файле /etc/protocols. Обычно задается протокол tcp для потокового соединения и udp для дейтаграмм; поле должно принимать значение wait или nowait. Поле должно иметь значение wait, если используется способ взаимодействия, не использующий соединение или приложение является сервером, который запускается один раз и принимает столько соединений, сколько будет запрошено. Если приложение является многопоточным или способ взаимодействия использует соединение, поле должно быть установлено в значение nowait; поле задает с идентификатором какого пользователя должен быть запущен процесс; эти поля задают запускаемую программу и аргументы запускаемого процесса.
Опции гнезд Следующие функции используются для получения и установки опций гнезда. Функции определены в файле <sys/socket.h>. int getsockopt (int SOCKET, int LEVEL, int OPTNAME, void *OPTVAL, socklen_t *OPTLEN-PTR)
Функция возвращает информацию о значении опции OPTNAME и уровня LEVEL для гнезда SOCKET. Значение опции сохраняется в буфере, на который указывает OPTVAL. перед вызовом в OPTLEN-PTR необходимо установить размер буфера. При завершении выполнения это поле содержит количество байтов, в действительности сохраненных в буфере. Для большинства опций значение имеет тип int. Функция возвращает 0 в случае успешного выполнения, -1 - в случае ошибки. переменная errno может принимать следующие значения: EBADF SOCKET не является дескриптором файла; ENOTSOCK SOCKET не является гнездом; ENOPROTOOPT опция OPTNAME не имеет смысла для заданного уровня LEVEL. int setsockopt (int SOCKET, int LEVEL, int OPTNAME, void *OPTVAL, socklen_t OPTLEN)
61
Эта функция устанавливает опцию OPTNAME на уровне LEVEL для гнезда SOCKET. Значение опции передается в буфере, на который указывает OPTVAL. Длина буфера задается в OPTLEN. Возвращаемое значение и коды ошибок идентичны предыдущей функции. Константа SOL_SOCKET, определенная в <sys/socket.h> используется для задания аргумента LEVEL. Имя опции может принимать следующие значения: SO_DEBUG эта опция включает запись отладочной информации. Значение имеет тип int; ненулевое значение означает “да”; SO_REUSEADDR опция показывает возможно ли повторное использование локального адреса гнезда. Значение опции имеет тип int; ненулевое значение означает “да”; SO_KEEPALIVE эта опция указывает должен ли драйвер протокола периодически посылать пакеты данных подключенному гнезду для контроля сохранения соединения. Если драйвер не получает ответа на эти пакеты, соединение признается нарушенным. Значение опции имеет тип int; ненулевое значение означает “да”; SO_DONTROUTE эта опция указывает должно ли сообщение проходить через стандартные средства маршрутизации или посылаться непосредственно к сетевому драйверу. Если значение установлено, то выполнение идет по второму варианту. Значение опции имеет тип int; ненулевое значение означает “да”; SO_LINGER опция определяет что должно происходить в случае, если остались неотправленные пакеты на момент закрытия гнезда. Значение имеет следующий тип: struct linger { int l_onoff; int l_linger; }
SO_BROADCAST SO_OOBINLINE SO_SNDBUF SO_RCVBUF SO_STYLE, SO_TYPE SO_ERROR
62
Если поле l_onoff имеет ненулевое значение, все оставшиеся данные передаются или драйвер ожидает время, заданное в поле l_linger. Поле l_linger задает тайм-аут в секундах; опция задает может ли дейтаграммы посылаться одновременно всем слушающим гнездам. Значение опции имеет тип int; ненулевое значение означает “да”; если опция установлена, чрезвычайные данные посылаются непосредственно в поток обычных данных. Значение опции имеет тип int; ненулевое значение означает “да”; опция позволяет установить или прочитать значение выходного буфера. Опция имеет тип size_t; опция позволяет установить или прочитать значение входного буфера. Опция имеет тип size_t; опция может использоваться только с getsockopt. Используется для получения способа взаимодействия. Опция имеет тип int, ее значение устанавливает способ взаимодействия; опция может использоваться только с getsockopt. Используется для сброса статуса ошибки гнезда.
Параллельность и псевдопараллельность Параллельностью называется параллельное выполнение двух или более процессов. Этот термин служит для описания общих принципов организации одновременного выполнения процессов в вычислительных системах, в частности, в мультипроцессорных системах. Многопроцессорные системы Вычислительные системы первого поколения предполагали использование однопроцессорной архитектуры, состоящей из одного центрального процессора (ЦП), памяти и периферийных устройств. Многопроцессорная архитектура, напротив, включает в себя два и более ЦП, совместно использующих общую память и периферийные устройства, располагая большими возможностями в увеличении производительности системы, связанными с одновременным исполнением процессов на разных ЦП. Каждый ЦП функционирует независимо от других, но все они работают с одним и тем же ядром операционной системы. Поведение процессов в такой системе ничем не отличается от поведения в однопроцессорной системе - с сохранением семантики обращения к каждой системной функции - но при этом они могут открыто перемещаться с одного процессора на другой. Параллельная работа нескольких процессоров в режиме ядра по выполнению различных процессов создает ряд проблем, связанных с сохранением целостности данных и решаемых благодаря использованию соответствующих механизмов защиты. Проблемы, связанные с многопроцессорными системами Первой из проблем является проблема разделения ресурсов. Действительно, представим себе две программы, пытающиеся печатать что-то на одном принтере. Если они будут делать это произвольным образом,их вывод на бумаге будет перемешан и скорее всего окажется совершенно нечитаемым. Одним из разумных решений может быть монопольный захват принтера одной из программ. При этом другая программа будет вынуждена ждать, пока принтер не освободится. Значит, нужны средства для захвата ресурсов и ожидания их освобождения. Другая проблема - это проблема реентерабельности (reenterability - повтоpной входимости, от re-enter) разделяемых программ. Представим себе, что две задачи используют общий программный модуль. Примером такого модуля может являться само ядро ОС. Представим себе, что одна из программ вызвала процедуру из разделяемого модуля. После этого произошло переключение задач, и вторая задача обращается к тому же модулю. Существует техника реализации этого модуля, при которой такой вызов не создаст никаких проблем. Такая техника состоит в том, что все данные, с которыми работает разделяемая программа, хранятся в ее локальных переменных. Для каждой инкарнации программы создается собственная копия таких данных. Такое требование легко выполняется для функций, не имеющих побочного эффекта, таких как вычисление синуса или поиск символа в текстовой строке. При других техниках программирования могут возникнуть серьезные проблемы, вплоть до развала системы. Модули, которые можно вызывать многократно, называются реентерабельными. Соответственно программы, которые так вызывать нельзя, называются нереентерабельными.
63
Легко понять, что программы, управляющие внешними устройствами или файловыми системами и вообще работающие с разделяемыми объектами, не могут быть реентерабельными. Во многих случаях оказывается выгодно рассматривать нереентерабельную программу как разделяемый ресурс и распределять доступ к нему теми же способами, что и к принтеру. Таким же образом микроядро организует доступ пользовательских программ к модулям ОС. Очень широкий класс проблем - проблемы синхронизации, возникающие при попытках организовать взаимодействие нескольких программ. Первая, самая простая из них, состоит в вопросе - если одна задача производит данные, а вторая их потребляет, то как задача-потребитель узнает, что готова очередная порция данных? Или, что еще интереснее, как она узнает, что очередная порция данных еще не готова? Типичный случай такого взаимодействия - асинхронное чтение с диска, когда программа дает дисковому драйверу запрос: “читай с такого-то сектора в такой-то блок памяти”, и продолжает заниматься своими делами. Другая проблема называется проблемой критических секций. Например, одна программа вставляет данные в разделяемый двунаправленный список, а другая достает их оттуда. Те, кто знаком с алгоритмом вставки в список, легко поймут, что есть момент, когда указатели элементов показывают вовсе не туда, куда надо. Попытка произвести в этот момент какую-то другую операцию изменения списка приведет к полному разрушению его структуры, а чтение или поиск закончатся аварией. Поэтому изменяющая программа должна каким-то образом блокировать доступ к списку на время изменения. Часто это делается теми же средствами, что и разделение ресурсов. Большинство решений всех вышеперечисленных проблем сводится к созданию какого-то средства, сообщающего программе, что произошло то или иное внешнее или внутреннее событие. При этом программа может остановиться, ожидая заданного события. К методам синхронизации процессов относятся следующие средства Linux: •сигналы; •семафоры; •блокировка участков файлов; •гармонически взаимодействующие последовательные процессы. Сигналы были рассмотрены в одной из предыдущих глав. Семафоры Если необходимо спроектировать простой и удобный механизм синхронизации, лучше всего объединить проверку флага и засыпание процесса в единую операцию, которая не может быть прервана исполнением другого процесса. Распространив эту идею на случай взаимодействия более чем двух процессов, мы приходим к механизму, известному под названием семафоров Дийкстры. Семафор Дийкстры представляет собой целочисленную переменную, с которой ассоциирована очередь ожидающих процессов. Пытаясь пройти через семафор, процесс пытается вычесть из значения переменной 1. Если значение переменной больше или равно 1, процесс проходит сквозь семафор успешно (семафор открыт). Если переменная равна нулю (семафор закрыт), процесс останавливается и ставится в очередь. Закрытие семафора соответствует захвату ресурса, доступ к которому контролируется этим семафором. Процесс, закрывший семафор, захватывает ресурс. Если ресурс захвачен, остальные процессы вынуждены ждать его освобождения.
64
Закончив работу с ресурсом, процесс увеличивает значение семафора на единицу, открывая его. При этом первый из стоявших в очереди процессов активизируется, вычитает из значения семафора единицу, и снова закрывает семафор. Если же очередь была пуста, то ничего не происходит, просто семафор остается открытым. Тогда первый процесс, подошедший к семафору, успешно пройдет через него. Наиболее простым случаем семафора является двоичный семафор. Начальное значение флаговой переменной такого семафора равно 1, и вообще она может принимать только значения 1 и 0. Двоичный семафор соответствует случаю, когда с разделяемым ресурсом в каждый момент времени может работать только одна программа. Семафоры общего вида могут принимать любые неотрицательные значения. Это соответствует случаю, когда несколько процессов могут работать с ресурсом одновременно, или когда ресурс состоит из нескольких независимых, но равноценных частей - например, несколько одинаковых принтеров. При работе с такими семафорами часто разрешают процессам вычитать и добавлять к флаговой переменной значения, большие единицы. Это соответствует захвату/освобождению нескольких частей ресурса. При доступе к нескольким различным ресурсам с использованием семафоров возникает специфическая проблема, называемая мертвой блокировкой (dead lock). Рассмотрим две программы, использующие доступ к двум различным ресурсам. Например, один процесс копирует данные со стримера на кассету Exabyte, а другой - в обратном направлении. Доступ к стримеру контролируется семафором sem1, а к кассете - семафором sem2. Первая программа сначала закрывает семафор sem1, затем sem2. Вторая программа поступает наоборот. Поэтому, если вторая программа получит управление и защелкнет sem2 в промежутке между соответствующими операциями первой программы, то мы получим мертвую блокировку - первая программа никогда не освободит sem1, потому что стоит в очереди у sem2, занятого второй программой, которая стоит в очереди у sem1, занятого первой... Все остальные программы, пытающиеся получить доступ к стримеру или кассете, также будут становиться в соответствующие очереди и ждать, пока администратор не убьет одну из зависших программ. Эта проблема может быть решена несколькими способами. Первый способ разрешить программе в каждый момент времени держать закрытым только один семафор - прост и решает проблему в корне, но часто оказывается неприемлемым. Более приемлемым оказывается соглашение, что семафоры всегда должны закрываться в определенном порядке. Этот порядок может быть любым, важно только чтобы он всегда соблюдался. Третий, наиболее радикальный, вариант состоит в предоставлении возможности объединить семафоры и/или операции над ними в неразделяемые группы. При этом программа может выполнить операцию закрытия семафоров sem1 и sem2 единой командой, во время исполнения которой никакая другая программа не может получить доступ к этим семафорам. Можно показать, что любые проблемы взаимодействия и синхронизации процессов могут - с большими или меньшими трудностями - быть решены при помощи семафоров. На практике трудности иногда оказываются не то, чтобы слишком большими, но нежелательными. Поэтому большинство ОС кроме семафоров предоставляет и другие средства синхронизации.
65
Так например, семафоры удобны при синхронизации доступа к единому ресурсу, такому как принтер. Если же нам нужна синхронизация доступа к ресурсу, имеющему внутреннюю структуру, такому как файл с базой данных, удобнее оказываются другие методы. Рассмотрим внутренние структуры данных Linux, с которыми имеют дело семафоры. /* Структура данных semid для каждого множества семафоров системы */ struct semid_ds { struct ipc_perm sem_perm; time_t sem_otime; time_t sem_ctime; struct sem *sem_base; struct wait_queue *eventn; struct wait_queue *eventz; struct sem_undo *undo; ushort sem_nsems; };
Операции с этой структурой проводятся с помощью системных вызовов. Поля структуры означают следующее: sem_perm Это пример структуры ipc_perm, которая описана в linux/ipc.h. Она содержит информацию о доступе к множеству семафоров, включая права доступа и информацию о создателе множества (uid и т.д.); sem_otime Время последней операции semop(). Описывается чуть позже; sem_ctime Время последнего изменения структуры; sem_base Указатель на первый семафор в массиве; sem_undo Число запросов undo в массиве (описывается ниже); sem_nsems Количество семафоров в массиве. В semid_ds есть указатель на базу массива семафоров. Каждый элемент массива имеет тип sem, который описан в linux/sem.h: /* Структура для каждого семафора в системе */ struct sem { short sempid; ushort semval; ushort semncnt; ushort semzcnt; };
sem_pid ID процесса, проделавшего последнюю операцию; sem_semval Текущее значение семафора; sem_semncnt Число процессов, ожидающих освобождения требуемых ресурсов; sem_semzcnt Число процессов, ожидающих освобождения всех ресурсов. Функция semget() используется для того, чтобы создать новое множество семафоров или получить доступ к старому. int semget ( key_t key, int nsems, int semflg )
В случае успешного выполнения функция возвращает идентификатор множества семафоров, -1 - в случае ошибки. Переменная errno может принимать следующие значения: EACCESS доступ отклонен; EEXIST идентификатор уже существует, создание нового невозможно; EIDRM множество помечено как удаляемое; ENOENT множество не существует, ни разу не была исполнена команда IPC_CREAT; ENOMEM не хватает памяти для новых семафоров; ENOSPC превышен лимит на количество множеств семафоров.
66
Первый аргумент semget() - это ключ. Он сравнивается с ключами остальных множеств семафоров, присутствующих в системе. Вместе с этим решается вопрос о выборе между созданием и подключением к множеству семафоров в зависимости от аргумента semflg. Возможные значения: IPC_CREAT Создает множество семафоров, если его еще не было в системе; IPC_EXCL При использовании вместе с IPC_CREAT вызывает ошибку, если семафор уже существует. Если IPC_CREAT используется в одиночку, то semget() возвращает идентификатор множества семафоров - вновь созданного или с таким же ключом. Если IPC_EXCL используется совместно с IPC_CREAT, то либо создается новое множество, либо, если оно уже существует, вызов приводит к ошибке и -1. Сам по себе IPC_EXCL бесполезен, но вместе с IPC_CREAT он дает средство гарантировать, что ни одно из существующих множеств семафоров не открыто для доступа. Аргумент nsems определяет число семафоров, которых требуется породить в новом множестве. Максимальное число семафоров определяется в "linux/sem.h": #define SEMMSL
32 /* <= 512 */
Аргумент nsems игнорируется, если вы открвываете существующую очередь. int semop( int semid, struct sembuf *sops, unsigned nsops)
Функция возвращает 0 в случае успешного выполнения, -1 - в случае ошибки. Переменная errno может принимать следующие значения: E2BIG nsops больше чем максимальное число позволенных операций; EACCESS доступ отклонен; EAGAIN при установленном флаге IPC_NOWAIT операция не может быть выполнена; EFAULT sops указывает на ошибочный адрес; EIDRM множество семафоров уничтожено; EINTR сигнал получен во время сна; EINVAL множество не существует или неверный semid; ENOMEM поднят флаг SEM_UNDO, но не хватает памяти для создания необходимой undo-структуры; ERANGE значение семафора вышло за пределы допустимых значений. Первый аргумент semop() есть значение ключа (в нашем случае возвращается semget). Второй аргумент (sops) - это указатель на массив операций, выполняемых над семафорами, третий аргумент (nsops) является количеством операций в этом массиве. Аргумент sops указывает на массив типа sembuf. Эта структура описана в linux/sem.h следующим образом: /* системный вызов semop требует массив таких структур */ struct sembuf { ushort sem_num; /* индекс семафора в массиве */ short sem_op; /* операция над семафором */ short sem_flg; /* флаги */ };
sem_num sem_op
Номер семафора, с которым вы собираетесь иметь дело; Выполняемая операция (положительное, отрицательное число или нуль); sem_flg Флаги операции. Если sem_op отрицателен, то его значение вычитается из семафора (семафор уменьшается - перев.). Это соответствует получению ресурсов, которые контролирует семафор. Если IPC_NOWAIT не установлен, то вызывающий процесс засыпает, пока семафор не выдаст требуемое количество ресурсов (пока другой процесс не освободит их).
67
Если sem_op положителен, то его значение добавляется к семафору. Это соответствует возвращению ресурсов множеству семафоров приложения. Ресурсы всегда нужно возвращать множеству семафоров, если они больше не используются! Наконец, если sem_op равен нулю, то вызывающий процесс будет усыплен (sleep()), пока значение семафора не станет нулем. Это соответствует ожиданию того, что ресурсы будут использованы на 100%. Хорошим примером был бы демон, запущенный с суперпользовательскими правами, динамически регулирующий размеры множества семафоров, если оно достигло стопроцентного использования. Чтобы пояснить вызов semop, вспомним нашу комнату с принтерами. Пусть мы имеем только один принтер, способный выполнять только одно задание за раз. Мы создаем множество семафоров из одного семафора (только один принтер) и устанавливаем его начальное значение в 1 (только одно задание за раз). Каждый раз, посылая задание на принтер, нам нужно сначала убедиться, что он свободен. Мы делаем это, пытаясь получить от семафора единицу ресурса. Заполним массив sembuf, необходимый для выполнения операции: struct sembuf sem_lock = { 0, -1, IPC_NOWAIT };
Трансляция вышеописанной инициализации структуры добавит -1 к семафору 0 из множества семафоров. Другими словами, одна единица ресурсов будет получена от конкретного (нулевого) семафора из нашего множества. IPC_NOWAIT установлен, поэтому либо вызов пройдет немедленно, либо будет провален, если принтер занят. Рассмотрим пример инициализации функции sembuf: if(semop(sid, %sem_lock, 1) == -1) perror("semop");
Третий аргумент (nsops) говорит, что мы выполняем только одну операцию (есть только одна структура sembuf в нашем массиве операций). Аргумент sid является идентификатором для нашего множества семафоров. Когда задание на принтере выполнится, необходимо вернуть ресурсы обратно множеству семафоров, чтобы принтером могли пользоваться другие. struct sembuf sem_unlock = { 0, 1, IPC_NOWAIT };
Трансляция вышеописанной инициализации структуры добавляет 1 к семафору номер 0 множества семафоров. Другими словами, одна единица ресурсов будет возвращена множеству семафоров. int semctl ( int semid, int semnum, int cmd, union semun arg )
Функция возвращает положительное число в случае успеха, -1 - в случае ошибки. Переменная errno может принимать следующие значения: EACCESS доступ отклонен; EFAULT адрес, указанный аргументом arg, ошибочен; EIDRM множество семафоров удалено; EINVAL множество не существует или неправильный semid; EPERM EUID не имеет привилегий для cmd в arg; ERANGE значение семафора вышло за пределы допустимых значений. Функция выполняет операции, управляющие множеством семафоров. Вызов semctl используется для осуществления управления множеством семафоров. Первый аргумент semctl() является ключом (в нашем случае возвращаемым вызовом semget). Второй аргумент (semun) - это номер семафора, над которым совершается операция. По существу, он может быть понят как индекс на множестве семафоров, где первый семафор представлен нулем. Аргумент cmd представляет собой команду, которая будет выполнена над множеством. Здесь снова присутствуют IPC_STAT/IPC_SET вместе с множеством дополнительных команд, специфичных для множеств семафоров: IPC_STAT Берет структуру semid_ds для множества и запоминает ее по адресу 68
аргумента buf в объединении semun; IPC_SET Устанавливает значение элемента ipc_perm структуры semid_ds для множества; IPC_RMID Удаляет множество из ядра; GETALL Используется для получения значений всех семафоров множества. Целые значения запоминаются в массиве элементов unsigned short, на который указывает член объединения array; GETNCNT Выдает число процессов, ожидающих ресурсов в данный момент; GETPID Возвращает PID процесса, выполнившего последний вызов semop; GETVAL Возвращает значение одного семафора из множества; GETZCNT Возвращает число процессов, ожидающих стопроцентного освобождения ресурса; SETALL Устанавливает значения семафоров множества, взятые из элемента array объединения; SETVAL Устанавливает значение конкретного семафора множества как элемент val объединения. Аргумент arg вызова semсtl() является примером объединения semun, описанного в linux/sem.h следующим образом: /* аргумент arg для системного вызова semctl */ union semun { int val; /* значение для SETVAL-а */ struct semid_ds *buf; /* буфер для IPC_STAT и IPC_SET */ ushort *array; /* массив для GETALL и SETALL */ struct seminfo *__buf; /* буфер для IPC_INFO */ void *__pad; };
Значения полей следующие: Определяет значение, в которое устанавливается семафор командой SETVAL; buf Используется командами IPC_STAT/IPC_SET. Представляет копию внутренней структуры данных семафора, находящейся в ядре; array Указатель для команд GETALL/SETALL. Ссылается на массив целых, используемый для установки или получения всех значений семафоров в множестве; __buf, __pad Предназначены для ядра и почти, а то и вовсе не нужны разработчику приложения. Эти два аргумента специфичны для LINUX, их нет в других системах UNIX. Поскольку этот особенный системный вызов наиболее сложен для восприятия среди всех системных вызовов System V IPC, мы рассмотрим несколько его примеров в действии. Следующий отрывок выдает значение указанного семафора. Последний аргумент (объединение) игнорируется, если используется команда GETVAL. val
int get_sem_val( int sid, int semnum ) { return( semctl(sid, semnum, GETVAL, 0)); }
Программа (semtool), приведенная в следующем примере, может использоваться для интерактивной работы с семафорами. Поведение semtool()-а зависит от аргументов командной строки, что удобно для вызова из скрипта shell-а. Позволяет выполнять операции от создания и манипулирования до редактирования прав доступа и удаления множества семафоров. Может быть использовано для управления
69
разделяемыми ресурсами через стандартные скрипты shell. Синтаксис командной строки: Создание множества семафоров semtool c (количество семафоров в множестве) Запирание семафора semtool l (номер семафора для запирания) Отпирание семафора semtool u (номер семафора для отпирания) Изменение прав доступа (mode) semtool m (mode) Удаление множества семафоров semtool d Примеры использования semtool: semtool semtool semtool semtool semtool
c 5 l u m 660 d
#include #include #include #include #include #include
<stdio.h> <stdlib.h> <sys/types.h> <sys/ipc.h> <sys/sem.h>
#define SEM_RESOURCE_MAX 1 /* Начальное значение для всех семафоров */ void opensem(int *sid, key_t key); void createsem(int *sid, key_t key, int members); void locksem(int sid, int member); void unlocksem(int sid, int member); void removesem(int sid); unsigned short get_member_count(int sid); int getval(int sid, int member); void dispval(int sid, int member); void changemode(int sid, char *mode); void usage(void); int main(int argc, char *argv[]) { key_t key; int semset_id; if(argc ==1) usage(); /* Создаем особый ключ через вызов ftok() */ key = ftok(".", 's'); switch(tolower(argv[1][0])) { case 'c': if(argc != 3) usage(); createsem(&semset_id, key, atoi(argv[2])); break; case 'l': if(argc != 3) usage(); opensem(&semset_id, key); locksem(semset_id, atoi(argv[2])); break; case 'u': if(argc != 3) usage(); opensem(&semset_id, key); unlocksem(semset_id, atoi(argv[2])); break;
70
case 'd': opensem(&semset_id, key); removesem(semset_id); break; case 'm': opensem(&semset_id, key); changemode(semset_id, argv[2]); break; default: usage(); } }
return(0);
void opensem(int *sid, key_t key) { /* Открываем множество семафоров - не создаем! */ if((*sid = semget(key, 0, 0666)) == -1) { printf("Semaphore set does not exist!\n"); exit(1); } } void createsem(int *sid, key_t key, int members) { int cntr; union semun semopts; if(members > SEMMSL) { printf("Sorry, max number of semaphores in a set is %d\n", SEMMSL); exit(1); } printf("Attempting to create new semaphore set with %d members\n", members); if((*sid = semget(key, members, IPC_CREAT|IPC_EXCL|0666)) == -1) { fprintf(stderr, "Semaphore set already exists!\n"); exit(1); } semopts.val = SEM_RESOURCE_MAX; /* Инициализируем все элементы (может быть сделано с SETALL) */ for(cntr=0; cntr<members; cntr++) semctl(*sid, cntr, SETVAL, semopts); }void locksem(int sid, int member) { struct sembuf sem_lock={ 0, -1, IPC_NOWAIT }; if( member<0 || member>(get_member_count(sid)-1)) { fprintf(stderr, "semaphore member %d out of range\n", member); return; } /* Попытка запереть множество семафоров */ if(!getval(sid, member)) { fprintf(stderr, "Semaphore resources exhausted (no lock)!\n"); exit(1);
71
} sem_lock.sem_num = member; if((semop(sid, &sem_lock, 1)) == -1) { fprintf(stderr, "Lock failed\n"); exit(1); } else printf("Semaphore resources decremented by one (locked)\n"); }
dispval(sid, member);
void unlocksem(int sid, int member) { struct sembuf sem_unlock={ member, 1, IPC_NOWAIT }; int semval; if( member<0 || member>(get_member_count(sid)-1)) { fprintf(stderr, "semaphore member %d out of range\n", member); return; } /* Заперто ли множество семафоров? */ semval = getval(sid, member); if(semval == SEM_RESOURCE_MAX) { fprintf(stderr, "Semaphore not locked!\n"); exit(1); } sem_unlock.sem_num = member; /* Попытка запереть множество семафоров */ if((semop(sid, &sem_unlock, 1)) == -1) { fprintf(stderr, "Unlock failed\n"); exit(1); } else printf("Semaphore resources incremented by one (unlocked)\n"); }
dispval(sid, member);
void removesem(int sid) { semctl(sid, 0, IPC_RMID, 0); printf("Semaphore removed\n"); } unsigned short get_member_count(int sid) { union semun semopts; struct semid_ds mysemds; semopts.buf = &mysemds;
}
/* Выдает количество элементов во множестве семафоров */ return(semopts.buf->sem_nsems);
int getval(int sid, int member)
72
{
}
int semval; semval = semctl(sid, member, GETVAL, 0); return(semval);
void changemode(int sid, char *mode) { int rc; union semun semopts; struct semid_ds mysemds; /* Получаем текущее значение для внутренней структуры данных */ semopts.buf = &mysemds; rc = semctl(sid, 0, IPC_STAT, semopts); if (rc == -1) { perror("semctl"); exit(1); } printf("Old permissions were %o\n", semopts.buf->sem_perm.mode); /* Изменяем права доступа к семафору */ sscanf(mode, "%ho", &semopts.buf->sem_perm.mode); /* Обновляем внутреннюю структуру данных */ semctl(sid, 0, IPC_SET, semopts); printf("Updated...\n"); } void dispval(int sid, int member) { int semval:
}
semval = semctl(sid, member, GETVAL, 0); printf("semval for member %d is %d\n", member, semval);
void usage(void) { fprintf(stderr, semaphores\n"); fprintf(stderr, fprintf(stderr, fprintf(stderr, fprintf(stderr, fprintf(stderr, exit(1); }
"semtool - A utility for tinkering with "\nUSAGE: semtool4 (c)reate <semcount>\n"); " (l)ock <sem #>\n"); " (u)nlock <sem #>\n"); " (d)elete\n"); " (m)ode <mode>\n");
Блокировка участков файлов Если говорить именно о файле с базой данных, оказывается удобно блокировать доступ к участкам файла. При этом целесообразно ввести два типа блокировок: на чтение и на запись. Блокировка на чтение разрешает другим процессам читать из заблокированного участка и даже ставить туда такую же блокировку, но запрещает писать в этот участок и, тем более, блокировать его на запись.
73
Этим достигается уверенность в том, что структуры данных, считываемые из захваченного участка, никем не модифицируются, поэтому гарантирована их целостность и непротиворечивость. В свою очередь, блокировка на запись запрещает всем, кроме блокирующего процесса, любой доступ к заблокированному участку файла. Это означает, что данный участок файла сейчас будет модифицироваться, и целостность данных в нем не гарантирована. В Linux возможны два режима блокировки: допустимая (advisory) и обязательная (mandatory). Как та, так и другая блокировка может быть блокировкой на чтение либо на запись. Допустимая блокировка является “блокировкой для честных”: она не оказывает влияния на подсистему ввода/вывода, поэтому программа, не проверяющая блокировок или игнорирующая их, сможет писать или читать из заблокированного участка без проблем. Обязательная блокировка требует больших накладных расходов, но запрещает физический доступ к файлу: чтение или запись, в зависимости от типа блокировки. При работе с разделяемыми структурами данных в ОЗУ было бы удобно иметь аналогичные средства, но их реализация ведет к большим накладным расходам, даже на системах с виртуальной памятью, поэтому ни одна из известных авторам систем таких средств не предоставляет. Впрочем, в современных версиях системы UNIX есть возможность отображать файл на виртуальную память. Используя при этом допустимую блокировку участков файла, программы могут синхронизовать доступ к нему (обязательная блокировка делает невозможным отображение на память). Гармонически взаимодействующие последовательные процессы Разделяемые структуры данных являются источником серьезных ошибок при разработке программ. Легко показать, что критические секции может иметь только программа, работающая с разделяемыми структурами данных. И этими критическими секциями как раз и являются места, где программа модифицирует такие структуры или просто обращается к ним. Синхронизация доступа к разделяемым структурам часто приводит к усложнению программы, а стремление сократить участки исключительного доступа - к ошибкам. Желание устранить эти проблемы привело в свое время Дийкстру к концепции, известной как гармонически взаимодействующие последовательные процессы. Эта концепция состоит в следующем: • Каждый процесс представляет собой независимый программный модуль, для которого создается иллюзия чисто последовательного исполнения; • Процессы не имеют разделяемых данных; • Все обмены данными, и вообще взаимодействие, происходят в выделенных точках процессов. В этих точках процесс, передающий данные, останавливается и ждет, пока его партнер будет готов эти данные принять. В некоторых реализациях UNIX процесспередатчик может не ожидать приема, а просто складывать данные в системный буфер. Аналогично, процесс, принимающий данные, ожидает, пока ему передадут данные. Иными словами, все передачи данных неразрывно связаны с синхронизацией. Синхронизация, не сопровождающаяся передачей данных, просто лишена смысла - процессы, не имеющие разделяемых структур данных, совершенно независимы и не имеют ни критических точек, ни нереентерабельных модулей. Концепция гармонически взаимодействующих процессов очень привлекательна с теоретической точки зрения и позволяет легко писать правильные программы. Однако часто по соображениям производительности оказывается невозможно отказаться от
74
разделяемой памяти. В этом смысле разделяемая память напоминает оператор goto. И то, и другое является потенциальным источником ошибок и проблем, но часто без них оказывается нельзя обойтись. В современных системах реализован целый ряд средств, которые осуществляют передачу данных одновременно с синхронизацией, например, каналы (pipes) в UNIX.
75