При проектировании высокопроизводительных сетевых приложения с неблокирующими сокетами важно решить, какой именно метод мониторинга сетевых событий мы будем использовать. Их есть несколько и каждый хорош и плох по-своему. Выбор правильного метода может быть критически важной вещью для архитектуры вашего приложения.
В этой статье мы рассмотрим:
Использование select()
Старый, проверенный годами работяга select() создавался ещё в те времена, когда «сокеты» назывались «сокетами Беркли». Данный метод не вошел в самую первую спецификацию тех самих сокетов Беркли, поскольку в те времена вообще ещё не существовало концепции неблокирующего ввода-вывода. Но где-то в 80-ых годах она появилась, а вместе с ней и select(). С тех пор в его интерфейсе ничего существенно не менялось.
Для использования select() разработчику необходимо инициализировать и заполнить несколько структур fd_set дескрипторами и событиями, которые необходимо мониторить, а затем уже вызвать select(). Типичный код выглядит примерно вот так:
1-й Урок: Знакомство с таблицами. Вывод столбцов. Операторы SELECT и FROM | SQL для всех
fd_set fd_in, fd_out; struct timeval tv; // обнуляем структуры FD_ZERO( FD_ZERO( // Будем мониторить события о входящих данных для sock1 FD_SET( sock1, // Будем мониторить события об исходящих данных для sock2 FD_SET( sock2, // Определим сокет с максимальным числовым значением (select требует это значение) int largest_sock = sock1 > sock2 ? sock1 : sock2; // Будем ждать до 10 секунд tv.tv_sec = 10; tv.tv_usec = 0; // Вызываем select int ret = select( largest_sock + 1, fd_out, NULL, // Проверяем успешность вызова if ( ret == -1 ) // ошибка else if ( ret == 0 ) // таймаут, событий не произошло else < if ( FD_ISSET( sock1, fd_out ) ) // исходящее событие на sock2 >
Когда проектировался select() никто, вероятно, не ожидал, что в будущем у нас появится необходимость писать многопоточные приложения, обслуживающие тысячи соединений. У select() есть сразу несколько существенных недостатков, делающих его плохо пригодным для работы в такого рода системах. Основными являются следующие:
- select модифицирует передаваемые ему структуры fd_sets, так что ни одну из них нельзя переиспользовать. Даже если вам не нужно ничего менять (например, получив порцию данных, вы хотите получить ещё) структуры fd_sets придётся переинициализировать. Ну или копировать из заранее сохранённого бэкапа с помощью FD_COPY. И это придётся делать снова и снова, перед каждым вызовом select.
- Для выяснения того, какой именно дескриптор сгенерировал событие, вам придётся вручную опросить их все с помощью FD_ISSET. Когда вы мониторите 2000 дескрипторов, а событие произошло лишь для одного из них (который по закону подлости будет последним в списке) — вы потратите уйму процессорных ресурсов впустую.
- Я только что упомянул 2000 дескрипторов? Это я погорячился. select не поддерживает так много. Ну, по крайней мере на обычном линуксе, с обычным ядром. Максимальное количество одновременно наблюдаемых дескрипторов ограниченно константой FD_SETSIZE, которая в линуксе жестко равна 1024. Некоторые операционные системы позволяют реализовать хак с переопределением значения FD_SETSIZE перед включением заголовочного файла sys/select.h, но этот хак не является частью какого-то общего стандарта. Тот же Linux проигнорирует его.
- Вы не можете работать с дескрипторами из наблюдаемого набора из другого потока. Представьте себе поток, выполняющий вышеуказанный код. Вот он запустился и ждёт событий в своём select(). Теперь представьте, что у вас есть ещё один поток, мониторящий общую нагрузку на систему, и вот он решил, что данные от сокета sock1 не приходят уже слишком давно и пора бы разорвать соединение. Поскольку данный сокет может быть переиспользован для обслуживания новых клиентов, хорошо бы его корректно закрыть. Но ведь первый поток наблюдает в том числе и за этим дескриптором прямо сейчас. Что же будет, если мы его всё-таки закроем? О, у документации есть ответ на этот вопрос и он вам не понравится: «Если дескриптор, наблюдаемый при помощи select(), будет закрыт другим потоком, вы получите неопределённое поведение».
- Та же проблема появляется и при попытке отправить какие-то данные через sock1. Мы ничего не отправим, пока select не закончит свою работу.
- Выбор событий, которые мы можем мониторить, достаточно ограничен. Например, для определения того, что удалённый сокет был закрыт, вам следует, во-первых, мониторить события прихода данных по нему, а во-вторых, сделать попытку чтения этих данных (read вернёт 0 для закрытого сокета). Это ещё можно назвать приемлемым при чтении данных из сокета (прочитали 0 — сокет закрыт), но что, если наша текущая задача в данный момент — отправка данных этому сокету и никакое чтение данных из него нам сейчас не нужно?
- select накладывает на вас излишнее бремя вычисления «наибольшего дескриптора» и передачу его отдельным параметром
Первая причина — портируемость. select() с нами уже миллион лет. В какие бы дебри программно-аппаратных платформ вас не занесло, если там есть сеть — там будет и select. Там может не быть никаких других методов, но select будет практически гарантированно. И не думайте, что я сейчас впадаю в старческий маразм и вспоминаю что-то типа перфокарт и ENIAC, нет.
Тотальное руководство по select в JavaScript и HTML. Закрываем гештальт фронтенд разработчика
Более современного метода poll нет, например, в Windows XP. А вот select есть.
Вторая причина более экзотична и имеет отношению к тому факту, что select может (теоретически) работать с таймаутами порядка одной наносекунды (если позволит аппаратная часть), в то время как и poll и epoll поддерживают лишь миллисекундную точность. Это не должно играть особой роли на обычных десктопах (или даже серверах), где у вас всё равно нет аппаратного таймера наносекундной точности. Но всё же в мире есть системы реального времени, имеющие такие таймеры. Так что я вас умоляю, когда будете писать прошивку ядерного реактора или ракеты — не поленитесь измерять время до наносекунд. Я, знаете ли, хочу жить.
Описанный выше случай, вероятно, единственный в котором у вас и правда нет выбора, что использовать (подходит лишь select). Однако, если вы пишете обычное приложение для работы на обычном железе, и вы будете оперировать адекватным количеством сокетов (десятками, сотнями — и не больше), то разница в производительности poll и select будет не заметна, так что выбор будет основываться на других факторах.
Опрос с помощью poll()
Давайте начнём с недостатков epoll — они очевидны из кода. Данный метод сложнее использовать, нужно написать больше кода, он делает больше системных вызовов.
Достоинства тоже налицо:
- epoll возвращает список только тех дескрипторов, для которых реально произошли наблюдаемые события. Не нужно просматривать тысячи структур в поисках той, возможно, одной, где сработало ожидаемое событие.
- Вы можете ассоциировать некоторый значимый контекст с каждым наблюдаемым событием. В примере выше мы использовали для этого указатель на объект класса соединения — это сэкономило нам ещё один потенциальный поиск по массиву соединений.
- Вы можете добавлять или удалять сокеты из списка в любое время. Вы можете даже модифицировать наблюдаемые события. Всё будет работать корректно, это официально поддерживается и задокументировано.
- Можно завести сразу несколько потоков, ожидающих события из одной и той же очереди с помощью epoll_wait. Нечто, что никоим образом не получится сделать с select/poll.
- Изменение флагов событий (например, переключение с READ на WRITE) требует лишнего системного вызова epoll_ctl, в то время как для poll вы просто меняете битовую маску (полностью в пользовательском режиме). Переключение 5000 сокетов с чтения на запись потребует для epoll 5000 системных вызовов и переключений контекста, в то время как для poll это будет тривиальная битовая операция в цикле.
- Для каждого нового соединения вам придётся вызвать accept() и epoll_ctl() — это два системных вызова. В случае использования poll вызов будет лишь один. При очень коротком времени жизни соединения это может иметь значение.
- epoll есть только в Linux. В других ОС есть схожие механизмы, но всё же не полностью идентичные. Вам не удастся написать код с epoll так, чтобы он собрался и заработал, например, на FreeBSD.
- Писать высоконагруженный параллельный код — тяжело. Многим приложениям не нужен столь фундаментальный подход, поскольку их уровень нагрузки легко обрабатывается и более простыми методами.
- Ваше приложение использует пул потоков для обработки сетевых соединений. Выигрыш от epoll в однопоточном приложении будет ничтожен, не стоит и заморачиваться на реализацию.
- Вы ожидаете относительно большого числа соединений (от 1000 и выше). На небольшом количестве наблюдаемых сокетов epoll не даст прироста производительности, а если сокетов буквально несколько штук — может даже замедлить.
- Ваши соединения живут относительно долго. В ситуации, когда новое соединение передаёт буквально несколько байт данных и тут же закрывается — poll будет работать быстрее, ведь на обработку ему нужно будет делать меньше системных вызовов.
- Вы намерены запускать ваш код на Linux и только на Linux.
libevent
libevent — это библиотека, которая «оборачивает» методы опроса, перечисленные в данной статье (а также некоторые другие) в унифицированный API. Преимущество здесь в том, что, однажды написав код, вы можете собрать и запустить его на разных операционных системах. Тем не менее, важно понимать, что libevent — это всего лишь обёртка, внутри которой работают всё те же вышеперечисленные методы, со всеми их преимуществами и недостатками. libevent не заставит select слушать более 1024 сокетов, а epoll — модифицировать список событий без дополнительного системного вызова. Так что знать лежащие в основе технологии по-прежнему важно.
Необходимость поддерживать разные методы опроса приводит к усложнению API библиотеки libevent. Но всё же его использование проще, чем вручную писать два разных движка выборки событий для, например, Linux и FreeBSD (используя epoll и kqueue).
Рассмотреть возможность использования libevent стоит при сочетании двух событий:
- Вы рассмотрели методы select и poll и они вам точно не подошли
- Вам нужно поддерживать несколько ОС
Источник: habr.com
Мультиплексирование ввода/вывода и асинхронный ввод/вывод
Аннотация: В ходе этой лекции вы изучите: использование системного вызова select, использование системного вызова poll, некоторые аспекты использования select/poll в многопоточных программах, стандартные средства асинхронного ввода/вывода.
Что может быть глупее, чем ждать?
Б. Гребенщиков
Системный вызов select
Если ваша программа главным образом занимаетсяоперациями ввода/вывода, вы можете получить наиболее важные из преимуществ многопоточности в однопоточной программе, используя системный вызов select(3C) . В большинстве Unix-систем select является системным вызовом, или, во всяком случае, описывается в секции системного руководства 2 (системные вызовы), т.е. ссылка на него должна была бы выглядеть как select(2) , но в Solaris 10 соответствующая страница системного руководства размещена в секции 3C ( стандартная библиотека языка С).
Устройства ввода/вывода обычно работают гораздо медленнее центрального процессора, поэтому при выполнении операций с ними процессор обычно оказывается вынужден ждать их. Поэтому во всех ОС системные вызовы синхронного ввода/вывода представляют собой блокирующиеся операции .
Это относится и к сетевым коммуникациям — взаимодействие через Интернет сопряжено с большими задержками и, как правило, происходит через не очень широкий и/или перегруженный канал связи .
Если ваша программа работает с несколькими устройствами ввода/вывода и/или сетевыми соединениями, ей невыгодно блокироваться на операции , связанной с одним из этих устройств, ведь в таком состоянии она может пропустить возможность совершить ввод/ вывод с другого устройства без блокировки. Эту проблему можно решать при помощи создания нитей, работающих с различными устройствами. В предыдущих лекциях мы изучили все необходимое для разработки таких программ. Однако для решения этой проблемы есть и другие средства.
Системный вызов select(3C) позволяет ожидать готовности нескольких устройств или сетевых соединений (в действительности, готовности объектов большинства типов, которые могут быть идентифицированы файловым дескриптором). Когда один или несколько из дескрипторов оказываются готовы передать данные, select(3C) возвращает управление программе и передает списки готовых дескрипторов в выходных параметрах.
В качестве параметров select(3C) использует множества (наборы) дескрипторов. В старых Unix-системах множества были реализованы в виде 1024-разрядных битовых масок. В современныхUnix-системах и в других ОС, реализующих select , множества реализованы в виде непрозрачного типа fd_set , над которым определены некоторые теоретико-множественные операции , а именно — очистка множества , включение дескриптора в множество, исключение дескриптора из множества и проверка наличия дескриптора в множестве. Препроцессорные директивы для выполнения этих операций описаны на странице руководства select(3C) .
В 32-разрядных версиях Unix SVR4 , в том числе в Solaris, fd_set по прежнему представляет собой 1024-битовую маску; в 64-разрядных версиях SVR4 это маска разрядности 65536 бит . Размер маски определяет не только максимальное количество файловых дескрипторов в наборе, но и максимальный номер файлового дескриптора в наборе. Размер маски в вашей версии системы можно определить во время компиляции по значению препроцессорного символа FD_SETSIZE . Нумерация файловых дескрипторов в Unix начинается с 0 , поэтому максимальный номер дескриптора равен FD_SETSIZE-1 .
Таким образом, если вы используете select(3C) , вам необходимо установить ограничения на количество дескрипторов вашего процесса. Это может быть сделано шелловской командой ulimit(1) перед запуском процесса или системным вызовом setrlimit(2) уже во время исполнения вашего процесса. Разумеется, setrlimit(2) необходимо вызвать до того, как вы начнете создавать файловые дескрипторы.
Если вам необходимо использовать более 1024 дескрипторов в 32-битной программе, Solaris 10 предоставляет переходный API . Для его использования необходимо определить препроцессорный символ FD_SETSIZE с числовым значением, превышающим 1024, перед включением файла . При этом в файле сработают необходимые препроцессорные директивы и тип fd_set будет определен как большая битовая маска , а select и другие системные вызовы этого семейства будут переопределены для использования масок такого размера.
В некоторых реализациях fd_set реализован другими средствами, без использования битовых масок. Например, Win32 предоставляет select в составе так называемого Winsock API . В Win32 fd_set реализован как динамический массив , содержащий значения файловых дескрипторов. Поэтому вам не следует полагаться на знание внутренней структуры типа fd_set .
Так или иначе, изменения размера битовой маски fd_set или внутреннего представления этого типа требуют перекомпиляции всех программ, использующих select(3C) . В будущем, когда архитектурный лимит в 65536 дескрипторов на процесс будет повышен, может потребоваться новая версия реализации fd_set и select и новая перекомпиляция программ. Чтобы избежать этого и упростить переход на новую версию ABI , компания Sun Microsystems рекомендует отказываться от использования select(3C) и использовать вместо него системный вызов poll(2) . Системный вызов poll(2) рассматривается далее на этой лекции.
Системный вызов select(3C) имеет пять параметров.
- int nfds — число, на единицу большее, чем максимальный номер файлового дескриптора во всех множествах, переданных как параметры.
- fd_set *readfds — Входной параметр, множество дескрипторов, которые следует проверять на готовность к чтению. Конец файла или закрытие сокета считается частным случаем готовности к чтению. Регулярные файлы всегда считаются готовыми к чтению. Также, если вы хотите проверить слушающий сокет TCP на готовность к выполнению accept(3SOCKET) , его следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к чтению.
- fd_set *writefds — Входной параметр, множество дескрипторов, которые следует проверять на готовность к записи. Ошибка при отложенной записи считается частным случаем готовности к записи. Регулярные файлы всегда готовы к записи. Также, если вы хотите проверить завершение операции асинхронного connect(3SOCKET) , сокет следует включить в это множество. Также, выходной параметр, множество дескрипторов, готовых к записи.
- fd_set *errorfds — Входной параметр, множество дескрипторов, которые следует проверять на наличие исключительных состояний. Определение исключительного состояния зависит от типа файлового дескриптора. Для сокетов TCP исключительное состояние возникает при приходе внеполосных данных. Регулярные файлы всегда считаются находящимися в исключительном состоянии. Также, выходной параметр, множество дескрипторов, на которых возникли исключительные состояния.
- struct timeval * timeout — тайм-аут, временной интервал, задаваемый с точностью до микросекунд. Если этот параметр равен NULL , то select(3C) будет ожидать неограниченное время; если в структуре задан нулевой интервал времени, select(3C) работает в режиме опроса, то есть возвращает управление немедленно, возможно с пустыми наборами дескрипторов.
Вместо всех параметров типа fd_set * можно передать нулевой указатель . Это означает, что соответствующий класс событий нас не интересует. select(3C) возвращает общее количество готовых дескрипторов во всех множествах при нормальном завершении (в том числе при завершении по тайм-ауту), и -1 при ошибке.
В примере 8.1 приводится использование select(3C) для копирования данных из сетевого соединения на терминал , а с терминала — в сетевое соединение. Эта программа упрощенная, она предполагает, что запись на терминал и в сетевое соединениеникогда не будет заблокирована. Поскольку и терминал , и сетевое соединение имеют внутренние буферы, при небольших потоках данных это обычно так и есть.
Пример 8.1 взят из книги У.Р. Стивенс, Unix: разработка сетевых приложений. Вместо стандартных системных вызовов используются «обертки», описанныев файле » unp.h «
#include «unp.h» void str_cli(FILE *fp, int sockfd) < int maxfdp1, stdineof; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; stdineof = 0; FD_ZERO( for ( ; ; ) < if (stdineof == 0) FD_SET(fileno(fp), FD_SET(sockfd, maxfdp1 = max(fileno(fp), sockfd) + 1; Select(maxfdp1, if (FD_ISSET(sockfd, /* socket is readable */ if (Readline(sockfd, recvline, MAXLINE) == 0) < if (stdineof == 1) return; /* normal termination */ else err_quit(«str_cli: server terminated prematurely»); >Fputs(recvline, stdout); > if (FD_ISSET(fileno(fp), /* input is readable */ if (Fgets(sendline, MAXLINE, fp) == NULL) < stdineof = 1; Shutdown(sockfd, SHUT_WR); /* send FIN */ FD_CLR(fileno(fp), continue; >Writen(sockfd, sendline, strlen(sendline)); > > >
8.1. Двустороннее копирование данных между терминалом и сетевым соединением.
Обратите внимание, что программа примера 8.1 заново пересоздает множества дескрипторов перед каждым вызовом select(3C) . Это необходимо, потому что при нормальном завершении select(3C) модифицирует свои параметры.
select(3C) считается MT-Safe, однако при его использовании в многопоточной программе надо иметь в виду следующий момент. Действительно, сам по себе select(3C) не использует локальных данных и поэтому его вызов из нескольких нитей не должен приводить к проблемам. Однако если несколько нитей работают с пересекающимися наборами файловых дескрипторов, возможен такой сценарий :
- Нить 1 включает дескриптор s в набор readfds и вызывает select .
- select в нити 1 возвращает s как готовый для чтения
- Нить 2 включает дескриптор s в набор readfds и вызывает select
- select в нити 2 возвращает s как готовый для чтения
- Нить 1 вызывает read из дескриптора s и получает все данные из его буфера
- Нить 2 вызывает read из дескриптора s и блокируется.
Чтобы избежать этого сценария, работу с файловыми дескрипторами в таких условиях следует защищать мутексами или какими-то другими примитивами взаимоисключения.
Важно подчеркнуть, что защищать надо не select , а именно последовательность операций над конкретным файловым дескриптором, начиная с включения дескриптора в множество для select и заканчивая приемом данных из этого дескриптора, точнее, обновлением указателей в буфере, в который вы считали эти данные. Если этого не сделать, возможны еще более увлекательные сценарии, например:
- Нить 1 включает дескриптор s в набор readfds и вызывает select .
- select в нити 1 возвращает s как готовый для чтения
- Нить 2 включает дескриптор s в набор readfds и вызывает select
- select в нити 2 возвращает s как готовый для чтения
- Нить 1 вызывает read из дескриптора s и получает только часть данных из его буфера
- Нить 2 вызывает read из дескриптора s , получает данные и записывает их поверх данных, полученных нитью 1
В «Архитектуры многопоточных приложений» мы рассмотрим архитектуру приложения, в котором несколько нитей работают с общим пулом файловых дескрипторов — так называемую архитектуру рабочих нитей ( worker threads). При этом нити, разумеется, должны указывать друг другу, с какими именно дескрипторами они сейчас работают.
С точки зрения разработки многопоточных программ, важным недостатком select(3C) — или, возможно, недостатком POSIX Thread API — является тот факт, что примитивы синхронизации POSIX не являются файловыми дескрипторами и не могут использоваться в select(3C) . В то же время, при реальной разработке многопоточных программ, занимающихся вводом/выводом, часто было бы полезно ожидать в одной операции готовности файловых дескрипторов и готовности других нитей собственного процесса.
Источник: intuit.ru
Что такое программа Select или селективная программа покупки автомобиля
На данный момент наблюдается рост рынка вторичных автомобилей. В первом квартале этого года было продано около одного миллиона шестьсот тысяч авто. Рост увеличился на 21 процент, если сравнивать с.
Прочитать полностью http://www.autoshcool.ru/4219-chto-takoe-programma. ramma-pokupki-avtomobilya.html
- Запись понравилась
- 0 Процитировали
- 0 Сохранили
- 0Добавить в цитатник
- 0Сохранить в ссылки
Источник: www.liveinternet.ru