
- •Операционные системы для программиста
- •Введение
- •1. Основные понятия
- •1.1. Понятие операционной системы
- •1.2. Системные соглашения для доступа к функциям ос
- •1.3. Особенности разработки программ в базовых ос
- •1.4. Командный интерфейс пользователя в ос
- •1.5. Информация об ошибках системной функции
- •2. Программный доступ к файловой системе
- •2.1. Понятия дескрипторов, идентификаторов и хэндлов
- •2.2. Ввод и вывод в стандартные файлы.
- •2.3. Базовые средства использования файлов
- •2.4. Многопользовательская блокировка файлов
- •2.5. Установка произвольной позиции в файле
- •3. Принципы построения ос
- •3.1. Модульная структура построения ос
- •3.2. Использование прерываний в ос
- •3.3. Управление системными ресурсами
- •3.4 Строение ядра операционной системы
- •3.5. Структура операционной системы типа Windows nt
- •4. Многофункциональный консольный вывод
- •4.1. Функции управления курсором
- •4.2. Многократный вывод символов и атрибутов
- •4.3. Вывод в произвольную позицию экрана
- •4.4. Ввод данных, размещенных предварительно на экране
- •5. Системные функции ввода для консольных устройств
- •5.1. Системные функции ввода текстовых строк
- •5.2. Событийно-управляемый ввод
- •5.3. Системные функции ввода с клавиатуры
- •5.4. Опрос ввода с клавиатуры в программе
- •5.5. Системные функции мыши для текстового режима
- •6. Файловые системы
- •6.1. Структуры файловых систем для пользователя
- •6.2. Методы распределения внешней памяти
- •6.3. Принципы построения файловых систем типа fat
- •6.4. Современные модификации файловой системы fat
- •6.5. Особенности построения файловой системы hpfs
- •6.6. Принципы построения файловой системы ntfs
- •6.7. Особенности строения файловых систем для Unix
- •6.8. Программный опрос файловой системы
- •7. Обеспечение множественности процессов
- •7.1. Основные понятия теории вычислительных процессов
- •7.2. Программное порождение процессов
- •7.3. Уничтожение процессов
- •7.4. Ожидание завершения процессов
- •8. Многопоточное функционирование ос
- •8.1. Понятие нити и связь Хе с процессом
- •8.2. Создание нитей (thread) в программе
- •8.3. Уничтожение нитей
- •8.4. Приостановка и повторный запуск нити
- •8.5. Ожидание завершения нити
- •9. Средства взаимодействия программных единиц
- •9.1. Абстрактные критические секции
- •9.2. Абстрактные семафоры
- •9.3. Семафоры взаимоисключения
- •9.4. Семафоры событий
- •9.5. Средства группового ожидания
- •9.6. Программные критические секции
- •9.7. Программные семафоры с внутренним счетчиком
- •10. Управление памятью
- •10.1. Виртуальная память
- •10.2. ЏодкРчка страниц для реализациШ виртуальной памяти
- •10.3. Системные функции распределения памяти
- •10.4. Совместное использование памяти
- •10.5. Отображение файлов в оперативную память
- •10.6. Динамически распределяемая память
- •11. Средства коммуникации процессов
- •11.1. Неименованные коммуникационные каналы Unix
- •11.2. Переназначение хэндлов для доступа к каналу
- •11.3. Неименованные каналы в Windows
- •11.4. Именованные каналы в Windows nt
- •11.5. Именованные каналы в Unix
- •12. Взаимодействие пользователя с ос
- •12.1. Интерфейсы операционных систем
- •12.2. Командные и операционные оболочки (shells)
- •12.3. Основные команды базовых операционных систем
- •12.4. Групповое выполнение и фоновый запуск команд
- •12.5. Стандартный ввод-вывод и конвейеры командной строки
- •12.6. Командные файлы и сценарии
- •Библиографический список
11. Средства коммуникации процессов
11.1. Неименованные коммуникационные каналы Unix
Коммуникационные каналы (pipe) относятся к основным средствам межпроцессорных взаимодействий (IPC – Inter Process Communication). Эти каналы в русскоязычной технической литературе называются также каналами передачи данных. Возникли они в составе операционной системы Unix, где до сих пор играют большую неявную роль при построении командных конвейеров, широко используемых в программировании на уровне командных оболочек.
Канал передачи данных отчетливо ассоциируется с трубопроводами, которые передают данные между процессами. Такой канал (или "труба") имеет два конца, в один данные поступают, из другого выходят. С точки зрения программиста, использующего канал, чтение данных из него и запись данных в канал можно выполнять, используя обычные операции чтения из файла и записи в файл на основе хэндлов.
В ходе своего исторического развития каналы передачи данных "мутировали" и к настоящему времени мы имеем две их разновидности: неименованные каналы и именованные каналы. Первая разновидность до сих пор широко используется в Unix, где ею обычно и ограничиваются. В дальнейшей части текущего раздела будем называть для сокращения неименованные каналы просто каналами, оставляя уточнения для дальнейшего изложения.
По классическому решению в Unix канал после создания имеет два конца, один из них может быть использован только для записи, а другой – только для чтения. (Ряд версий Unix расширил такое понимание канала и теперь в общем случае можно использовать оба конца канала как для записи, так и для чтения, но делать так не рекомендуется, потому что подобные действия чрезвычайно усложняют для программиста процесс действий с каналом.) По внутреннему строению неименованный канал представляет собой кольцевой буфер в памяти, разделяемой процессами, причем этот буфер снабжен внутренними указателями. Один из них задает место, с которого можно далее писать данные, а второй – место с которого начинаются еще не считанные данные.
Для создания канала передачи данных в Unix предназначена функция с прототипом
int pipe(int hpipe[2]).
Аргументом этой системной функции служит адрес массива для двух хэндлов, младший элемент этого массива при удачном выполнении функции дает хэндл для конца канала, с которого можно считывать данные, а второй элемент дает при этом хэндл конца канала, в который можно записывать данные.
Для записи в канал используется функция write, а для чтения – функция read. Причем применение функции read к каналам имеет небольшую специфику, которая на самом деле присутствует и для аналогичной операции с обычными файлами, но там она не бросается в глаза. Дело в том, что функция read возвращает как собственное значение число реально прочитанных байтов. Для файлов отличие числа прочитанных байтов от числа запрошенных оказывается равносильным ситуации конца файла. Строго говоря, согласно документации, ситуацию конца файла определяет только возврат нулевого значения в качестве числа байтов, прочитанных функцией read чтения из файла. Но для обычных файлов после получения меньшего числа байтов чем запрошено, не существует альтернативы обязательному получению нулевого числа байтов в результате следующего выполнения функции чтения.
Для каналов же передачи данных вычислительный поток (нить процесса в общем случае) приостанавливается на функции чтения из канала до тех пор, пока канал функционирует, но данные еще не поступили. Если же в выходной конец канала поступило сколько-то байтов, то независимо от их числа функция чтения возвращает сколько возможно с учетом запрошенного. В частности, возвращает меньше байтов чем запрошено, но такое получение данных может не иметь никакого отношения к завершению данных в канале и возникновению ситуации конец файла. После чтения какого-то числа байтов при очередном запросе данных функцией read канал может выдать следующую порцию данных, в лучшем случае ровно столько, сколько запрошено, но может опять вернуть меньше запрошенного.
Можно сказать, что инженерной "изюминкой" работы с каналами передачи данных является предоставление каналом того числа байтов, которые уже поступили через канал, не дожидаясь поступления всей запрошенной партии байтов. Таким образом, при работе с каналами передачи данных ситуация конца передачи данных определяется возвращением нулевого значения функцией чтения.
Другим существенным моментом организации программистом работы с каналом является формирование ситуации конца передачи данных (конца файла для читающей стороны). Таким средством является принудительное закрытие канала со стороны конца, в который происходит запись данных. С этой стороны выполняется операция close(хэндл_конца_записи).
Следующая программа, приведенная в листинге 11.1.1, демонстрирует использование канала передачи данных в Unix. Для такой демонстрации создается два вычислительных процесса на основе одной общей программы. Процесс-родитель, остающийся от исходного процесса после выполнения системного вызова fork(), определив по ненулевому коду возврата этой функции, что он именно родитель, выдает сообщение об этом и закрывает свой хэндл hpipe[1] для конца записи. Сам передающий конец канала при этом не закрывается, так как у него остается еще продублированный в дочернем процессе хэндл этого конца канала (Системный вызов fork() дублирует все, что возможно, для дочернего экземпляра процесса, в частности таблицы, которые задают соответствия хэндлов дескрипторам файлов.) Заметим, что для собственно закрытия передающего конца канала теперь необходимо закрытие хэндла hpipe[1] дочерним процессом, что последний и выполнит в свое время.
#include <stdio.h>
int main()
{int hpipe[2];
char buffer[18];
char txt[ ]="Text, for sending and demonstration pipes into Unix";
int off=0, actread, lentxt=sizeof(txt), len, pid;
printf("Begin working\n");
if (pipe(hpipe)= =-1) {perror("pipe");exit(1);}
if ((pid=fork())!=0)
{printf("I am Parent, and my Child ID=%d\n",pid);
close(hpipe[1]);
while(1)
{sleep(2);
actread=read(hpipe[0],buffer,11);
if (actread= =0) break;
buffer[actread]='\0';
printf("From pipe: %s len=%d\n",buffer,actread);
}
close(hpipe[0]);
printf("End Parent\n");
}
else
{printf("I am Child\n");
close(hpipe[0]);
while(1)
{len=(4<lentxt-off) ? 4:lentxt-off;
write(hpipe[1],&txt[off],len);
off+=len;
if (len!=4) break;
sleep(1);
}
close(hpipe[1]);
printf("End Child\n");
}
return 0;
}
Листинг 11.1.1. Использование неименованных каналов в Unix
Затем, используя дополнительную задержку на пару секунд для демонстрации продолжительности процесса, родитель запрашивает 11 байтов данных у канала. Возвращаемое значение actread функции read проверяется на предмет определения закрытия канала со стороны передающей стороны. При определения такого закрытия "бесконечный" цикл чтения прерывается. Любое число байтов, полученное в цикле от функции чтения из канала, выводится для наблюдения на экран консоли. По прерыванию цикла чтения (со стороны родителя) читающий конец канала закрывается, выдается сообщение о завершении родителя, и процесс завершается командой return 0.
Дочерний процесс, обнаружив по нулевому значению функции fork(), что он именно дочерний, в составном операторе после else сообщает о себе и выполняет закрытие hpipe[0]. (Читающий конец канала остается управляемым тем же исходном дескриптором этого конца канала, доступ к которому имеется в родительском процессе через хэндл hpipe[0].) В "бесконечном" цикле дочерний процесс подготавливает значение переменной len, задающей число подлежащих передачи следующих байтов данных через канал. (Это число выбирается равным 4, за исключением последней порции передачи данных, когда она может оказаться меньшей величины – в связи с некратностью длины передаваемого текста числу 4.) Очередная порция передаваемого текста, задаваемая смещением в переменной off и числом в переменной len передается посредством хэндла hpipe[1] через канал. После передачи корректируется величина смещения off и выполняется проверка, не передана ли последняя порция данных. Последняя ситуация определяется по тому, что значение вспомогательной переменной len оказывается меньшим начального значения 4 (последняя переданная порция меньше или равна нулю). По прерыванию оператором break цикла выполняется закрытие передающей стороны канала через закрытие хэндла hpipe[1]. Выдается сообщение о завершении дочернего процесса и он завершается оператором return 0.