Пять моделей ввода-вывода:
- блокирующий ввод-вывод (blocking I/O)
- неблокирующий ввод-вывод (nonblocking I/O)
- ввод-вывод с мультиплексированием (I/O multiplexing: select and poll)
- ввод-вывод, управляемый сигналом (signal driven I/O: SIGIO)
- асинхронный ввод-вывод (asynchronous I/O: the POSIX aio functions)
Обычно есть две отдельные фазы для операции ввода:
- Ожидание готовности данных
- Копирование данных из ядра в буфер процесса
Для операции ввода в сокет первый шаг обычно включает ожидание поступления данных в сеть. Когда пакет прибывает, он копируется в буфер в ядре. Второй шаг - копирование этих данных из буфера ядра в буфер текущего приложения.
Блокировки зло - так как замедляют работу процесса, длительность непредсказуема
Процесс блокирован все время от момента, когда был выполнен вызов recvfrom, до момента, когда дейтаграмма поступает и копируется в буфер приложения.
Не ожидает наличия данных (или возможности вывода), а получает результат выполнения операции или невозможность её выполнения в данный момент, что определяется по коду возврата.
Первые три раза системный вызов на чтение данные не возвращает, а возвращает ошибку EWOULDBLOCK. Следующий системный вызов выполняется успешно, так как данные готовы для чтения. Ядро скопирует данные в буфер приложения и будут обработаны. Такой режим работы называется опросом (polling), так как выполняется постоянный вызов, в данном случае, recvfrom.
Для организации такого опроса может быть создан цикл, вызывающий соответствующий системный вызов, следовательно, большие накладные расходам (overhead) – пустая трата процессорного времени.
При мультиплексировании ввода-вывода вызываются, так называемые мультиплексоры , select или poll и вместо блокировки в непосредственном системном вызове ввода-вывода процесс блокируется на одном из этих системных вызовов.
Приложение блокируется при вызове select, ожидая когда сокет станет доступным для чтения. Затем ядро возвращает приложению статус readable, сообщая, что можно получать данные помощью recvfrom. Снова блокировка, вместо одного два системных вызова (select и recvfrom), что увеличивает накладные расходы. Но в отличии от блокирующего метода, select или любой другой мультиплексор обеспечивает возможность ожидать данные не от одного, а от нескольких файловых дескрипторов, что, снижает время блокировки (сна).
- Когда данные готовы к считыванию ядро должно уведомить приложение и послать сигнал SIGIO.
- Приложение не блокируется, а продолжает работать несмотря на то, что данные не готовы. Всю работу берет на себя ядро:
- отслеживает готовность данных
- затем посылает сигнал SIGIO, который вызывает установленный на него обработчик (функция обратного вызова – callback)
В вызове aio_read() даётся указание ядру начать операцию ввода-вывода, и указывается, каким сигналом уведомить процесс о завершении операции (включая копирование данных в пользовательский буфер). При этом вызывающий процесс не блокируется, а результат операции (например, полученная UDP-дейтаграмма) может быть обработан в обработчике сигнала. Разница с предыдущей моделью ввода-вывода, который управляется сигналом, состоит в том, что в предыдущей модели сигнал уведомляет о возможности начала операции (вызове операции чтения), а в асинхронной модели сигнал уведомляет уже о завершении операции копирования данных в буфер пользователя.
Блокирующий синхронный ввод-вывод:
- процесс блокируется в ожидании завершения операции ввода-вывода;
- для информирования процессора о завершении операции ввода-вывода контроллер внешнего устройства формирует сигнал, который по линии IRQ№ поступает на контроллер прерывания;
- контроллер формирует сигнал прерывания, который по линии шины данных поступает на выделенный pin процессора;
- в конце цикла выполнения каждой команды процессор проверяет наличие сигнала прерывания и при его поступлении переходит на выполнение обработчика данного прерывания;
- обработчик прерывания должен сохранить данные в буфере ядра, чтобы затем функции обратного вызова драйвера устройства доставили их приложению. Для этого приложение разблокируется.
При неблокирующем синхронном вводе-выводе приложение не блокируется, а делает многочисленные запросы, проверяя готовность данных.
Асинхронный ввод-вывод (AIO) (неблокирующий асинхронный) - процесс не блокируется до завершения операции, он продолжает выполнять свой код и можно позже проверить статус поданного запроса. Для завершения транзакции ввода-вывода может использоваться сигнал, на который устанавливается обработчик сигнала.
В соответствии с данной классификацией:
- блокирующий ввод-вывод – блокирующий синхронный;
- неблокирующий ввод-вывод или опрос (polling) – синхронный неблокирующий;
- мультиплексированный ввод-вывод – блокирующий асинхронный;
- ввод-вывод, управляемый сигналами - неблокирующий асинхронный, но при получении сигнала о готовности данных вызывается блокирующий системный вызов.
- абстракция точки соединения (абстракция конечной точки взаимодействия). Используются для организации взаимодействия процессов (неважно, на одной или нескольких машинах)
Для связывания нужно указать адрес. В Unix-домене это текстовая строка - имя файла, через который происходит обмен данными. В Internet-домене адрес задаётся комбинацией IP-адреса и 16-битного номера порта. IP-адрес определяет хост в сети, а порт - конкретный сокет на этом хосте. Протоколы TCP и UDP используют различные пространства портов.
Протокол TCP - прежде, чем начать обмен данными, требуется установить соединение между двумя хостами. Имеет высокую надежность, поскольку позволяет не терять данные при передаче, запрашивает подтверждения о получении от принимающей стороны и в случае необходимости отправляет данные повторно. При этом отправляемые пакеты данных сохраняют порядок отправки, то есть можно сказать, что передача данных упорядочена. Минусом данного протокола является относительно низкая скорость передачи данных, за счет того что выполнение надежной и упорядоченной передачи занимает больше времени, чем в альтернативном протоколе UDP
Протоколы UDP - Для передачи данных ему не обязательно устанавливать соединение между отправителем и получателем. Информация передается без предварительной проверки готовности принимающей стороны. Это делает протокол менее надежным – при передаче некоторые фрагменты данных могут теряться. Кроме того, упорядоченность данных не соблюдается – возможен непоследовательный прием данных получателем. Зато скорость передачи данных по данному транспортному протоколу будет более высокой.
Взаимодействие параллельных процессов через сокеты выполняется по модели клиент-сервер.
Сервер предоставляет ресурсы и службы 1+ клиентам, которые обращаются к серверу за обслуживанием. Один сервер может
предоставлять ресурсы нескольким клиентам одновременно. Когда клиент запрашивает соединение с сервером, сервер может либо принять, либо отклонить это соединение.
Клиент не предоставляет общий доступ ни к одному из своих ресурсов, но запрашивает
данные или службу у сервера.
Для поверки состояния неблокирующих сокетов можно использовать мультиплексирование
Мультиплексирование ввода-вывода обычно используется сетевыми приложениями в случаях:
- Когда клиент обрабатывает множество дескрипторов (обычно интерактивный ввод и сетевой сокет).
- Возможно, хотя это и редкий случай, когда клиент одновременно обрабатывает множество сокетов.
- Если сервер TCP обрабатывает и прослушиваемый сокет, и присоединенные сокеты.
- Если сервер работает и с TCP, и с UDP.
- Если сервер обрабатывает несколько служб и, возможно, несколько протоколов (например, демон inetd).
используют в качестве адресов имена файлов специального типа Важной особенностью сокетов в файловом пространстве имён является то, что соединение с их помощью локального и удаленного приложений невозможно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе.
- Сокеты в программах представлены дескрипторами
- используется для хранения адресов
struct sockaddr
{
unsigned short sa_family; // Семейство адресов, AF_xxx (набор адресов, которые могут быть использованы для данного сокета)
char sa_data[14]; // 14 байтов для хранения адреса
};
sock = socket(AF_INET, SOCK_STREAM, 0);
- создаёт безымянный сокет (т.е. не связанный ни с локальным адресом, ни с номером порта). Прежде чем передавать данные через сокет, его необходимо связать с адресом в выбранном домене (эту процедуру называют именованием сокета)
- 1 параметр - домен
Домен, обозначенный константой AF_UNIX, соответствует сокетам в файловом пространстве имен (cемейство сокетов AF_UNIX используется для взаимодействия между процессами на одной машине). AF_INET - открываемый сокет должен быть сетевым
- 2 параметр - тип сокета.
- SOCK_DGRAM (датаграмный сокет) - допустимый тип сокеты в UNIX, использует "User Datagram Protocol", или "UDP" (без установления соединения, ненадежная передача сообщений, сообщения фиксированной максимальной длины)
- SOCK_STREAM - гарантируется доставка байт в порядке поступления; пока непрерывный поток байтов не прекратится, никакие другие данные приниматься каналом не будут (аналогом такой связи является pipe-механизм)(двустороннее соединение)
- SOCK_RAW - (сырой сокет, нижнего уровня) простой или символьный сокет.
- 3 параметр - протокол, используемый для передачи данных. 0 = протокол не указан, используется значение по умолчанию для данного вида соединений.
Возвращает целое положительное число - номер дескриптора сокета (то есть создается дескриптор сокета)
ssize_t sendto(int sockfd, const void * buf, size_t len, int flags, const struct sockaddr * dest_addr, socklen_t addrlen);
- отправляют данные в сокет
- используется при обмене данными с датаграммными сокетами (может м с потоковыми)
err = sendto(sock, buf, strlen(buf), 0, &srvr_name, sizeof(srvr_name));
1 параметр - дескриптор сокета
2 параметр - адрес буфера для передачи данных
3 параметр - длина буфера
4 параметр - дополнительных флагов
ПРОСТО ДЛЯ ОЗНАКОМЛЕНИЯ
может принимать значения:
MSG_CONFIRM - Сообщить уровню связи, что процесс пересылки произошел: получен успешный ответ с другой стороны. Если уровень связи не получит его, то он будет регулярно перепроверять наличие ответной стороны.
MSG_DONTROUTE - Не использовать маршрутизацию для отправки пакета, а посылать его только на узлы локальной сети. Обычно это используется в диагностических программах и программах маршрутизации.
MSG_DONTWAIT - Включить неблокирующий режим.
MSG_EOR - Завершить запись (record)
MSG_MORE - Дополнительные данные для отправки.
MSG_NOSIGNAL - Не генерировать сигнал SIGPIPE, если сторона потокоориентированного сокета закрыла соединение.
MSG_OOB - Послать внепоточные данные, если сокет это поддерживает.
5 параметр - Адрес сервера
6 параметр - Длина адреса сервера
При успешном выполнении эти вызовы возвращают количество отправленных байт
Для обмена датаграммами не нужно устанавливать соединение. Создав сокет с помощью socket и bind, его тут же можно использовать его для отправки или получения данных. Для этого вам понадобятся функции sendto (В ЭТОМ ОТЛИЧИЕ ОТ SEND) и recvfrom.
Перед вызовом функции sendto() надо заполнить структуру sockaddr (переменную srvr_name) данными об адресе сервера.
После окончания передачи данных сокет закрывается с помощью close().
- ЯВНОЕ СВЯЗЫВАНИЕ
- связывает сокет с заданным адресом (связывать сокет с адресом необходимо в программе-сервере, но не в клиенте)
- После вызова программа-сервер становится доступна для соединения по заданному адресу (имени файла).
bind(sock, &srvr_name, sizeof(srvr_name))
1 параметр - дескриптор сокета
2 параметр - указатель на структуру sockaddr (переменная srvr_name), содержащую адрес, на котором регистрируется сервер
3 параметр - длина структуры, содержащей адрес
int recvfrom(int s, void * buf, size_t len, int flags, struct sockaddr * from, socklen_t * fromlen);
- принимает данные из сокета (по умолчанию блокирует программу до тех пор, пока на входе не появятся новые данные)
- используется при обмене данными с датаграммными сокетами (может м с потоковыми)
len = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL);
1 параметр - дескриптор сокета
2 параметр - указатель на буфер, из которого будет производится чтение
3 параметр - число байт для чтения/записи
4 параметр - флаги (если не нужны, то 0)
ПРОСТО ДЛЯ ОЗНАКОМЛЕНИЯ
может принимать значения:
MSG_OOB запрашивает прием внепотоковых данных, которые в противном случае не были бы получены в обычном потоке данных.
MSG_PEEK - выбрать данные из начала очереди, но не удалять их оттуда. Таким образом, последующий вызов функции вернет те же самые данные.
MSG_WAITALL - подождать, пока не придет полное запрошенное количество данных. Однако, этот вызов все равно может вернуть меньше данных, чем было запрошено, если был пойман сигнал, произошла ошибка или разрыв соединения, или если начали поступать данные другого типа, не того, который был сначала.
MSG_TRUNC - реальная длину пакета, даже если она была больше, чем предоставленный буфер.
MSG_ERRQUEUE - Получить пакет из очереди ошибок.
MSG_NOSIGNAL отключает возникновение сигнала SIGPIPE на потоковых сокетах, если другая сторона вдруг исчезает.
MSG_ERRQUEUE - получить из очереди ошибок сокета накопившиеся ошибки.
5 параметр - адрес клиента
6 параметр - указатель на переменную, в которой будет возвращена длина структуры с адресом
Если информация об адресе клиента не нужна, то можно передать значения NULL в предпоследнем и последнем параметрах
Возвращает количество принятых байт
- По завершении работы с сокетом он «закрывается» с помощью «файловой» функции close().
- Перед выходом из программы-сервера следует удалить файл сокета, созданный в результате вызова socket(), что делается с помощью функции unlink().
- содержит имя сервера в приемлемом для дальнейшего использования виде
struct hostent
{
char FAR * h_name; // имя хоста
char FAR * FAR * h_aliases; // дополнительные названия
short h_addrtype; // тип адреса
short h_length; // длинна каждого адреса в байтах
char FAR * FAR * h_addr_list; // список адресов
};
- Для сетевого взаимодействия
struct sockaddr_in
{
short int sin_family; // Семейство адресов
unsigned short int sin_port; // Номер порта
struct in_addr sin_addr; // IP-адрес
unsigned char sin_zero[8]; // Дополнение до размера структуры sockaddr
};
- получает указатель на строку с Интернет-именем сервера (например, www.unix.com или 192.168.1.16)
- возвращает указатель на структуру hostent, которая содержит имя сервера в приемлемом для дальнейшего использования виде
- выполняет преобразование доменного имени сервера в его сетевой адрес
- для установки соединения (НЕЯВНОЕ СВЯЗЫВАНИЕ)
1 параметр - дескриптор сокета
Если сокет имеет тип SOCK_DGRAM, адрес serv_addr является адресом по умолчанию, куда посылаются датаграммы, и единственным адресом, откуда они принимаются. Если сокет имеет тип SOCK_STREAM, то данный системный вызов попытается установить соединение с другим сокетом.
2 параметр - адрес, на который он ссылается
3 параметр - длина адреса
Если успешно, возвращается ноль.
- отправляет данные в сокет
send(sock, buf, strlen(buf), 0)
1 параметр - дескриптор сокета
2 параметр - адрес буфера для передачи данных
3 параметр - длина буфера
4 параметр - дополнительных флагов
Возвращает количество отправленных символов
- буфер фиксированного размера
- выполняет дополнительную операцию над файловым дескриптором fd. Эта операция определяется содержимым аргумента cmd.
fcntl(sock, F_SETFL, O_NONBLOCK); По умолчанию функция socket() создает блокирующий сокет. Чтобы сделать его неблокирующим, надо использовать эту функцию с флагом O_NONBLOCK. Теперь любой вызов функции read() для сокета sock будет возвращать управление сразу же.
- F_SETFL - Устанавливает часть флагов
- сокет будет связан со всеми локальными интерфейсами
- то есть наша программа-сервер зарегистрируется на всех адресах той машины, на которой она выполняется.
- (принимает запросы от клиента с любым ip)
- переписывает двухбайтовое значение порта так, чтобы порядок байтов соответствовал сетевому (Порядок от старшего к младшему (англ. big-endian — с большого конца))
- сообщает сокету, что должны приниматься новые соединения (задать размер очереди)
- пассивное прослушивание
listen(sock, NUM)
1 параметр - дескриптор сокета
2 параметр - максимальное число соединений, которые сервер может обрабатывать одновременно
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- очищает набор
- добавляют заданный дескриптор к набору
- проверяет, является ли дескриптор частью набора
int select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * utimeout);
- контролировать несколько файловых дескрипторов, ожидающих, пока один или несколько файловых дескрипторов не станут доступны (готовы) для некоторого класса операций ввода / вывода. Дескриптор файла считается готовым, если возможно выполнить соответствующую операцию ввода / вывода
- Отслеживаются три независимых набора дескрипторов
err = select(max_fd + 1, &set, NULL, NULL, &interval);
1 параметр - целое число, на единицу большее максимального файлового дескриптора в любом из наборов
2 параметр - набор дескрипторов (для чтения)
3 параметр - набор дескрипторов (для записи)
4 параметр - набор дескрипторов для слежения за «исключительными ситуациями»
5 параметр - задаёт наибольшее время, которое вызов select() будет ожидать событий, по прошествии которого завершит работу, даже если ничего не произойдёт. Если значение этого аргумента равно NULL, то select() будет ожидать бесконечно. Значение utimeout может быть установлено в ноль секунд; в этом случае select() возвратит управление немедленно.
- использует timeout представленный struct timeval (with seconds and microseconds)
- может обновить аргумент timeout , чтобы указать сколько времени осталось
- не имеет аргумент sigmask
Возвращает номер файлового дескриптора
- НЕЯВНОЕ СВЯЗЫВАНИЕ
- используется для получения нового сокета для нового входящего соединения
- устанавливает соединение в ответ на запрос клиента и создает копию сокета для того, чтобы исходный сокет мог продолжать прослушивание
- получив запрос на соединение, возвращает новый сокет, открытый для обмена данными с клиентом, запросившим соединение
- Первоначальный сокет остается в состоянии прослушивания, а новый сокет – в состоянии CONNECTED. Такое дублирование сокетов при приеме соединения дает серверу возможность продолжать принимать новые соединения без необходимости предварительного закрытия предыдущих соединений.
int new_sock = accept(sock, NULL, NULL);
1 параметр - дескриптор сокета
2 параметр - сведения об адресе клиента, запросившего соединение
3 параметр - размер
Последние два параметра могут быть NULL