Основные понятия
Канал - это средство связи стандартного вывода одного процесса со стандартным вводом другого. Каналы - это старейший из инструментов IPC, существующий приблизительно со времени появления самых ранних версий оперативной системы UNIX. Они предоставляют метод односторонних коммуникаций (отсюда термин half-duplex) между процессами.
Эта особенность широко используется даже в командной строке UNIX (в shell-е). ls | sort | lp
Приведенный выше канал принимает вывод ls как ввод sort, и вывод sort за ввод lp. Данные проходят через полудуплексный канал, перемещаясь (визуально) слева направо.
Хотя большинство из нас использует каналы в программировании на shell-е довольно часто, мы редко задумываемся о том, что происходит на уровне ядра.
Когда процесс создает канал, ядро устанавливает два файловых дескриптора для пользования этим каналом. Один такой дескриптор используется, чтобы открыть путь ввода в канал (запись), в то время как другой применяется для получения данных из канала (чтение). В этом смысле, канал мало применим практически, так как создающий его процесс может использовать канал только для взаимодействия с самим собой. Рассмотрим следующее изображение процесса и ядра после создания канала: in <----- Process Kernel out ----->
Из этого рисунка легко увидеть, как файловые дескрипторы связаны друг с другом. Если процесс посылает данные через канал (fd0), он имеет возможность получить эту информацию из fd1. Однако этот простенький рисунок отображает и более глобальную задачу. Хотя канал первоначально связывает процесс с самим собой, данные, идущие через канал, проходят через ядро. В частности, в Linux-е каналы внутренне представлены корректным inode-ом. Конечно, этот inode существует в пределах самого ядра, а не в какой-либо физической файловой системе. Эта особенность откроет нам некоторые привелекательные возможности для ввода/вывода, как мы увидим немного позже.
Зачем же нам неприятности с созданием канала, если мы всего-навсего собираемся поговорить сами с собой? На самом деле, процесс, создающий канал, обычно порождает дочерний процесс. Как только дочерний процесс унаследует какой-нибудь открытый файловый дескриптор от родителя, мы получаем базу для мультипроцессовой коммуникации (между родителем и потомком). Рассмотрим эту измененную версию нашего рисунка: in <----- -----> in Parent Process Kernel Child Process out -----> <----- out
Теперь мы видим, что оба процесса имеют доступ к файловым дескрипторам, которые основывают канал. На этой стадии должно быть принято критическое решение. В каком направлении мы хотим запустить данные? Потомок посылает информацию к родителю или наоборот? Два процесса взаимно согласовываются и "закрывают" неиспользуемый конец канала. Пусть потомок выполняет несколько действий и посылает информацию к родителю обратно через канал. Наш новый рисунок выглядел бы примерно так: in <----- in Parent Process Kernel Child Process out <----- out
Конструкция канала теперь полная. Все, что осталось сделать - это использовать его. Чтобы получить прямой доступ к каналу, можно применять системные вызовы, подобные тем, которые нужны для ввода/вывода в файл или из файла на низком уровне (вспомним, что в действительности каналы внутренне представлены как корректный inode).
Чтобы послать данные в канал, мы используем системный вызов write(), а чтобы получить данные из канала - системный вызов read(). Вспомним, что системные вызовы ввода/вывода в файл или из файла работают с файловыми дескрипторами! (Однако, не забывайте, что некоторые системные вызовы, как, например, lseek(), не работают с дескрипторами.)