Скачать презентацию
Идет загрузка презентации. Пожалуйста, подождите
Презентация была опубликована 11 лет назад пользователемweb-local.rudn.ru
1 Архитектура вычислительных систем Константин Ловецкий Декабрь 2011 Кафедра систем телекоммуникаций 1 Проблема очередности действий и ее решение
2 При дейтаграммном соединении на сокете доступны две основные операции: передача пакета и прием пакета данных, причем размер пакета, вообще говоря, ограничен (но a priori размер этого ограничения неизвестен). Потоковый тип взаимодействия предоставляет прикладному программисту иллюзию надежного двунаправленного канала передачи данных. Данные могут быть записаны в канал порциями любого размера; гарантируется, что на другом конце данные либо будут получены без потерь и в том же порядке, либо не будут получены вообще (соединение в этом случае будет разорвано с фиксацией ошибки). Проблема очередности действий
3 При работе с сокетами потокового типа после принятия первого соединения в программе-сервере возникает проблема очередности действий. У нас имеются два дескриптора (а после установления соединения с новыми клиентами их число возрастет). На одном из дескрипторов может ожидать запрос на соединение нового клиента, который потребует вызова accept(). Но если мы сделаем accept(), а никакого запроса на соединение не было, наша программа так и останется внутри вызова accept(), причем пробыть там может сколь угодно долго: никто ведь не может гарантировать, что кто-либо захочет установить с нами новое соединение. В это время на любой из клиентских сокетов (то есть сокетов, отвечающих за соединение с каждым из клиентов) могут прийти данные, требующие обработки; ясно, что, пока мы висим внутри вызова accept(), никакой обработки данных не произойдет, т.к. мы их даже не прочитаем Проблема очередности действий
4 С другой стороны, если попытаться произвести чтение с того или иного клиентского сокета, есть риск, что клиент по каким-либо причинам не пришлет нам никаких данных в течение длительного периода времени. Все это время наша программа будет находиться внутри вызова read() или recv(), не выполняя никакой работы, в том числе не принимая новых соединений и не обрабатывая данные, поступившие от других (уже присоединенных) клиентов. Проиллюстрировать проблему можно на примере из совсем некомпьютерной области. Представим себе ресторан, очень дорогой, в котором каждый столик размещен в отдельной кабинке, так что, в каком бы месте мы ни стояли, нельзя одновременно увидеть все столики и входные двери. Представим себе теперь, что на весь ресторан есть только один официант, исполняющий еще и обязанности метродотеля. В нашей аналогии официант будет играть роль процесса.
5 Сразу после открытия ресторана официант, естественно, подойдет к входным дверям и будет поджидать новых клиентов (вызов accept()), чтобы, встретив их, проводить их к столику. Однако сразу после того, как первые клиенты займут столик и примутся изучать меню, официант окажется перед проблемой: следует ли ему стоять около столика в ожидании, пока клиенты определятся с заказом, или же следует пойти к дверям проверить, не пришел ли еще кто-нибудь. Проблема очередности действий
6 Когда же клиентов за столиками станет много, официанту и вовсе придется тяжело. Ведь каждому клиенту за любым из столиков в любой момент может что-то понадобиться он может решить заказать еще какое-либо блюдо, может случайно уронить на пол вилку и попросить принести новую, может, наконец, решить, что ему пора идти, и потребовать счет. С другой стороны, в любой момент могут прийти и новые клиенты, и если никто не встретит их у дверей, они могут обидеться и уйти, а ресторан недополучит денег. Напомним, что по условиям нашей аналогии официант не может найти такое место в ресторане, откуда было бы видно и столики, и входные двери; точнее говоря, из любого места видно либо один столик (но не больше), либо двери, либо вообще ни то, ни другое Проблема очередности действий
7 Самое простое решение, приходящее в голову это бегать по всему ресторану, подбегая по очереди к каждому из столиков, а также и к дверям, узнавать, что внимание официанта там не требуется и идти, вернее, бежать на следующий круг. Аналогичное «решение» возможно и для нашего процесса. Можно с помощью вызова fcntl() перевести все сокеты (и слушающий, и клиентские) в неблокирующий режим, при котором вызовы read() и accept() всегда возвращают управление немедленно, ничего не ожидая (если не было данных или входящего соединения, возвращается ошибка). После этого можно начать их опрашивать по очереди в бесконечном цикле. Это называется активным ожиданием Проблема очередности действий
8 Такой вариант считается совершенно неприемлемым в многозадачных системах. Аналогично тому, как официант будет попусту уставать, вхолостую нарезая круги по ресторану (вполне возможно, что за несколько таких кругов от него ничего никому так и не понадобится) и в итоге, скорее всего, попросту упадет от переутомления, процесс, бесконечно опрашивающий набор сокетов с помощью системных вызовов, тоже будет вхолостую тратить ресурсы. В отличие от официанта процесс не устанет, но он при этом будет понапрасну расходовать процессорное время, которое могло бы пригодиться другим задачам. В некоторых системах процесс может исчерпать свой лимит процессорного времени и будет попросту уничтожен ядром. Проблема очередности действий
9 Другое решение (для ресторана) состоит в том, чтобы нанять в ресторан адекватное количество персонала. Во- первых, разумеется, у дверей должен стоять метродотель, немедленно встречая каждых приходящих клиентов и проводя их к свободному столику. Во-вторых, дорогой ресторан вполне может себе позволить прикрепить к каждому столику своего официанта. Аналогичным образом при написании серверной программы мы можем создать отдельный процесс для обслуживания каждого пришедшего клиента. Если же этот вариант неприемлем (например, если большую часть времени весь персонал все равно простаивает), можно сделать и иначе Проблема очередности действий
10 Официант может, нарезав парочку кругов, сообразить, что так дело не пойдет и, например, подвесить колокольчик к входным дверям, а каждый столик снабдить кнопкой звонка. После этого можно будет спокойно усесться в укромный угол и спать (или, например, разгадывать кроссворд), пока один из колокольчиков или звонков не зазвонит. Поскольку ресторан имеет свойство в некий момент закрываться, в дополнение к этим звонкам следует, видимо, завести еще и будильник на определенное время. Аналогичный вариант в программировании называется мультиплексированием ввода-вывода. Рассмотрим два последних варианта подробнее. Проблема очередности действий
11 В этом варианте наш главный процесс выполняет обязанности метродотеля, находясь большую часть времени в вызове accept(). Приняв очередное соединение, «метродотель» порождает дочерний процесс (посылает «официанта») для обслуживания данного соединения. После порождения родительский процесс закрывает сокет клиентского соединения, а дочерний процесс закрывает слушающий сокет. Соответственно, все обязанности по обслуживанию пришедшего клиента возлагаются на дочерний процесс; после завершения сеанса связи с клиентом дочерний процесс завершается. Все это время родительский процесс беспрепятственно продолжает исполнять обязанности «метродотеля», вызывая accept(). Решение на основе обслуживающих процессов
12 Событийно-управляемое программирование. Рассмотрим случай, когда порождение отдельного процесса на каждое клиентское соединение неприемлемо. Это может получиться в случае, если сервер достаточно серьезно загружен: операция порождения процесса сравнительно дорога, так что при загрузках порядка тысяч соединений в секунду затраты на порождение процессов могут оказаться неприемлемыми. Кроме того, может оказаться, что между сеансами обслуживания разных клиентов происходит активное взаимодействие. Например, сервер может поддерживать компьютерную игру по сети, так что действия каждого игрока влияют на ситуацию в одном игровом пространстве и сказываются на других игроках. Мультиплексирование ввода-вывода
13 В этом случае разделение серверной программы на отдельные процессы потребует высоких накладных расходов на взаимодействие между этими процессами. К тому же проблема очередности действий встанет снова, только уже в каждом из процессов: действительно, следует ли процессу в конкретный момент времени анализировать изменения в игре или же принимать данные с клиентского сокета? Итак, необходимо оставить обслуживание всех клиентов в рамках одного процесса, причем активное ожидание, естественно, приемлемым не считается. Мультиплексирование ввода-вывода
14 На проблему можно взглянуть и шире. Есть некоторое количество типов событий, каждое из которых требует своей обработки. Некоторые системные вызовы, предназначенные для обработки событий, имеют такое свойство, что, будучи вызванными до наступления события, они этого события ожидают, блокируя вызвавший процесс и делая, таким образом, невозможной обработку других событий. С другой стороны, возможно и такое, что ни одно из событий в течение долгого периода времени не произойдет. При этом необходимо исключить холостой расход процессорного времени. Получается так, что нам необходима возможность отдать управление операционной системе, отказавшись от процессорного времени до тех пор, пока не произойдет одно из интересующих нас событий. Наступление события операционная система должна отследить сама и вернуть управление процессу, при этом, желательно, сообщив ему о том, какое именно событие наступило. Мультиплексирование ввода-вывода
15 В ОС Unix такой механизм предоставляют системные вызовы select() и poll(). Мы будем рассматривать вызов select() как более простой. Вызов select() позволяет обрабатывать события трех типов: изменение состояния файлового дескриптора (появление данных, доступных на чтение, или входящего запроса на соединение; освобождение места в буфере исходящей информации; исключительная ситуация); истечение заданного количества времени с момента входа в вызов; получение процессом неигнорируемого сигнала. Событийно-управляемое программирование
16 Профиль вызова выглядит следующим образом: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); Параметры readfds, writefds и exceptfds обозначают множества файловых дескрипторов, для которых нас интересует, соответственно, возможность немедленного чтения, возможность немедленной записи и наличие исключительной ситуации. Параметр n указывает, какое количество элементов в этих множествах является значащим. Этот параметр необходимо установить равным max_d+1, где max_d максимальный номер дескриптора среди подлежащих обработке. Наконец, параметр timeout задает промежуток времени, спустя который следует вернуть управление, даже если никаких событий, связанных с дескрипторами, не произошло. Событийно-управляемое программирование
17 Объект «множество дескрипторов» задается переменной типа fd_set. Внутренняя реализация переменных этого типа, вообще говоря, для различных систем может оказаться разной, но проще всего ее представлять себе как битовую строку, где каждому дескриптору соответствует один бит. Для работы с переменными этого типа система предоставляет в наше распоряжение следующие макросы: FD_ZERO(fd_set *set); /* очистить множество */ FD_CLR(int fd, fd_set *set);/* убрать дескриптор из мн-ва */ FD_SET(int fd, fd_set *set);/* добавить дескриптор к мн-ву */ FD_ISSET(int fd, fd_set *set); /* входит ли дескр-р в мн-во? */ Событийно-управляемое программирование
18 Структура timeval, служащая для задания последнего параметра, имеет два поля типа long. Поле tv_sec задает количество секунд, поле tv_usec количество микросекунд (миллионных долей секунды). Таким образом, например, задать тайм-аут в 5.3 секунды можно следующим образом: »struct timeval t; »t.tv_sec = 5; »t.tv_usec = ; В качестве любого из параметров, кроме первого (количества дескрипторов), можно указывать нулевой указатель, если задание данного параметра нам не требуется. Так, если нужно просто некоторое время подождать, можно указать NULL вместо всех трех множеств дескрипторов. Событийно-управляемое программирование
19 Вызов select() возвращает управление в следующих случаях: В случае, если произошла ошибка (в частности, в одном из множеств дескрипторов оказалось число, не соответствующее ни одному из открытых дескрипторов); в этом случае вызов возвращает -1. В случае, если программа получила неигнорируемый сигнал. В этом случае также возвращается -1; отличить эту ситуацию от ошибочной можно по значению глобальной переменной errno, которая в этом случае будет равна константе EINTR. Истек тайм-аут, то есть с момента входа в вызов прошло больше времени, чем указано в параметре timeout (если, конечно, этот параметр не был нулевым указателем). В этом случае вызов возвращает 0. Событийно-управляемое программирование
20 На какой-либо из дескрипторов, входящих в множество readfds, пришли данные, которые можно прочитать вызовом read() (то есть вызов read() не заблокируется); в случае слушающего сокета в роли данных выступают запросы на соединение, то есть, соответственно, можно гарантировать, что вызов accept() не заблокируется; в случае непотоковых сокетов гарантируется, что не будет заблокирован соответствующий вызов recvfrom() и т.п. Следует обратить внимание, что ситуация «конец файла» также истолковывается как готовность сокета на чтение, поскольку в этой ситуации вызов read() также не блокируется. Какой-либо из дескрипторов, входящих в writefds, готов к немедленной записи, то есть, если применить к нему вызов write(), send() или еще какой-то подобный, то он не заблокирует процесс. Событийно-управляемое программирование
21 Следует отметить, что большинство дескрипторов, открытых на запись, к записи готовы в любой момент, так что, если внести какой-то из них в множество writefds, вызов вернет управление немедленно. Обычно параметр writefds используется при передаче в сеть больших объемов данных, когда буфер исходящей информации может переполниться и стать причиной блокирования процесса на вызове write(). На каком-либо из дескрипторов, входящих во множество exceptfds, возникла исключительная ситуация. На самом деле, это возможно только на сетевых сокетах и только в случае использования механизма OOB (out-of-band), а он используется сравнительно редко. Поэтому и сам параметр exceptfds используется редко, обычно указывается NULL. В последних трех случаях вызов select() возвращает количество дескрипторов, изменивших статус. Событийно-управляемое программирование
22 Все множества дескрипторов, переданных вызову select(), в этом случае изменяются: в них остаются только те дескрипторы, статус которых изменился. Таким образом, проверив с помощью макроса FD_ISSET интересующие нас дескрипторы, можно узнать, на каком из них требуется выполнить операцию чтения (или принятия соединения, или записи, и т.п.) Таким образом, работу с вызовом select() можно построить по нижеприведенной схеме. В приведенном коде предполагается, что номер слушающего сокета хранится в переменной ls; организовать хранение дескрипторов клиентских сокетов можно самыми разными способами, в зависимости от задачи. Кроме того, предполагается, что out-of-band data не используется, и что передаваемые в сеть объемы данных невелики, так что используется только множество readfds. Событийно-управляемое программирование
23 Событийно-управляемое программирование
24 Событийно-управляемое программирование
25 Событийно-управляемое программирование
26 Мультиплексирование ввода-вывода
27 Способ построения программ, при котором программа имеет главный цикл, одна итерация которого соответствует наступлению некоторого события из определенного множества, а все действия программы построены как реакция на событие, называется событийно-управляемым программированием (англ. event-driven programming) Событийно-управляемое программирование
Еще похожие презентации в нашем архиве:
© 2024 MyShared Inc.
All rights reserved.