netlib.narod.ru | < Назад | Оглавление | Далее > |
Очереди сообщений рассматриваются первыми, поскольку они имеют простейшую реализацию, но при этом демонстрируют некоторые архитектурные особенности, общие для всех трех механизмов System V IPC.
Процессы получают четыре системных вызова, относящихся к очереди сообщений:
15919: Объект struct msg представляет единственное сообщение, ожидающее в очереди. Он имеет следующие члены:
15865: Объект msqid_ds представляет единственную очередь сообщений. Он имеет следующие члены:
msg_qbytes. Максимальное число байтов, которое разрешено использовать для хранения всех сообщений в очереди; msg_cbytes сравнивается с msg_qbytes для определения того, есть ли еще место для нового сообщения. Значение msg_qbytes принято по умолчанию равным MSGMNB, однако этот предел может быть увеличен динамически пользователем с соответствующими правами.
Величина MSGMNB установлена директивой #define равной 16384 байтам в строке 15904. Существуют четыре причины применения такого низкого предела. Во-первых, на практике обычно редко возникает необходимость включать так много информации в конкретное сообщение, поэтому данный предел не является слишком ограничительным. Во-вторых, если отправители сообщений будут забегать слишком далеко вперед по сравнению с получателями сообщений, то, вероятно, нет никакого смысла позволять им и дальше работать в таком же темпе; они получают возможность на время остановиться, чтобы позволить получателям справиться с полученным заданием. В-третьих, этот предел в 16 Кб на очередь может в принципе быть умножен на 128 очередей, что составляет в сумме 2 Мб.
Но основной причиной установки такого предела является предотвращение описанной выше атаки по принципу «отказа в обслуживании». Однако ничто не мешает приложениям посылать сообщения нулевой длины (т.е. пустые) сообщения. Эти сообщения не будут отражаться на величине msg_qbytes, а для заголовков сообщений все еще будет распределяться память, поэтому возможность атаки по принципу «отказа в обслуживании» все еще сохраняется. Один из способов решения этой проблемы состоит в наложении независимого предела на число сообщений, которым разрешено находиться в очереди; вторым решением является вычитание из msg_qbytes размера всего сообщения, включая заголовки. Третье решение, безусловно, состоит в запрещении пустых сообщений, но это привело бы к нарушению совместимости.
20129: Центральной структурой данных в реализации очереди сообщений является msgque — массив указателей на объекты struct msqid_ds. Существует всего MSGMNI таких указателей (эта величина установлена равной 128 с помощью директивы #define в строке 15900), что соответствует 128 очередям сообщений. Но почему бы просто не использовать массив объектов struct msqid_ds вместо массива указателей? Одной из причин этого является экономия места: вместо массива из 128 56-байтовых структур (7168 байтов или ровно 7 Кб) для msgque применяется массив из 128 4-байтовых указателей (512 байтов). В обычном случае, когда применяется ряд очередей сообщений, это позволяет сэкономить несколько килобайтов. В наихудшем случае, когда распределены все очереди сообщений, максимальный расход составляет 512 байтов. Единственным реальным недостатком является дополнительный уровень разадресации, что приводит к небольшому снижению быстродействия.
Зависимость между основными структурами данных очереди сообщений показана на рис. 9.1.
Рис. 9.1. Структуры данных очереди сообщений
20137: Функция msg_init инициализирует переменные, используемые в этой реализации очереди сообщений. Основная часть ее работы является лишней, поскольку переменные уже были установлены в эти значения в их объявлениях непосредственно перед функцией.
20141: Однако цикл, который устанавливает входы msgque в значение IPC_UNUSED, необходим. Значение IPC_UNUSED, не рассматриваемое в этой книге, принято равным –1 (соответственно, приведенной к void*ndash;*); оно представляет неиспользуемую очередь сообщений. Еще одним специальным значением, которое может принять вход msgque, но только временно, на время создания очереди сообщений, является значение IPC_NOID (также не рассматриваемое в этой книге).
20149: Функция real_msgsnd реализует содержательную часть функции sys_msgsnd, системного вызова msgsnd. Это нарушение принятого в ядре соглашения об именовании «реализующих функций» системных вызовов с префиксом «do_».
Вызов функции real_msgsnd осуществляется из строки 20338, где она находится внутри пары функций lock_kernel/unlock_kernel. (Эти функции рассматриваются в главе 10 и в каждый данный момент заблокировать ядро может только один процессор, что имеет смысл для компьютеров с симметричной мультипроцессорной архитектурой.) Это элегантный способ обеспечения выполнения функции unlock_kernel; в противном случае, сложное управление последовательностью выполнения функции real_msgsnd было бы дополнительно усложнено, в связи с необходимостью всегда обеспечивать вызов функции unlock_kernel на пути выхода.
Как известно, в ядре такие проблемы в основном решаются с помощью переменных с кодом возврата и операторов goto. Принятый в sys_msgsnd способ проще, но он может не сработать в некоторых случаях. Например, рассмотрим, что произойдет, если функция должна будет получить несколько ресурсов и к некоторым из них она должна будет обратиться, только если все предшествующие ресурсы были успешно получены. Примитивное решение по принципу расширения функции sys_msgsnd потребовало бы создания множества функций, примерно так, как показано ниже:
void f1(void) { if (acquire_resource1()) { f2(); release_resource1(); } } void f2(void) { if (acquire_resource2()) {] f3(); release_resource2(); } } void f3(void) { if (acquire_resource3()) { /* ... здесь выполняется настоящая работа ... */ release_resource3(); } }
Эта конструкция быстро становится громоздкой и поэтому она в ядре не применяется.
20158: Начинает последовательность проверки допустимости. Вторая из трех проверок в этой строке не нужна, если учесть первую проверку: любой размер сообщения, который не пройдет вторую проверку, не сможет пройти также и первую. Однако в будущем положение может измениться, если предельное значение MSGMAX будет увеличено в достаточной степени. (В действительности, во время написания данной книги проводилась работа по полному устранению предела MSGMAX.)
20164: Идентификаторы очереди сообщений кодируют два фрагмента информации: индекс соответствующего элемента msgque находится в младших 7 битах, а порядковый номер, который вскоре будет описан, находится в 16 битах непосредственно над ними. В данный момент достаточно знать часть, содержащую индекс массива.
20166: Если по указанному индексу массива нет очереди сообщений или таковая была здесь создана, то в нее не должно быть поставлено ни одного сообщения.
20171: Хранимый порядковый номер в очереди сообщений должен соответствовать номеру, закодированному в параметре msqid. Идея состоит в том, что одно лишь обнаружение очереди сообщений по правильному индексу массива не означает, что это — та очередь сообщений, которая нужна вызывающей программе. С того момента, как вызывающая программа получила свою ссылку на эту очередь, очередь сообщений, находившаяся первоначально по этому индексу, могла быть удалена и на ее месте создана новая. 16-разрядный порядковый номер постоянно наращивается, поэтому новая очередь по тому же индексу будет иметь порядковый номер, отличный от первоначального. (Если только не случилось так, что в этот промежуток времени было создано еще 65535 новых очередей, что весьма маловероятно, или создано еще 131071 новых очередей, что еще более маловероятно, и т.д. Однако, как описано далее в этой главе, в действительности не так все просто.) Во всяком случае, если порядковые номера не совпадают, функция real_msgsnd возвращает ошибку EIDRM в качестве указания на то, что очередь сообщений, нужная для вызывающей программы, была удалена.
20174: Проверка того, что вызывающая программа имеет разрешение на запись в очередь сообщений. Аналогичная система рассматривается более подробно в главе 11; пока достаточно отметить, что применяется система, полностью аналогичная проверке прав доступа к файлам Unix.
20177: Проверка того, не будет ли превышен максимально допустимый размер очереди после записи поступившего сообщения в очередь. В следующей строке выполняются абсолютно такие же действия, очевидно, из-за недосмотра при редактировании кода, оставшегося после преобразования ядра серии 2.0. Между двумя проверками раньше находился код, который иногда позволял освободить немного места в очереди.
20180: В очереди нет свободного места. Если в структуре msgflg установлен бит IPC_NOWAIT, вызывающая программа не должна ждать, пока это случится, поэтому возвращается ошибка EAGAIN.
20182: Процесс должен быть переведен в состояние ожидания. Вначале функция real_msgsnd проверяет, существует ли сигнал, ожидающий этого процесса. Если это так, это рассматривается как прерывание процесса этим сигналом (после чего он может снова перейти в состояние ожидания, как будет вскоре показано).
20184: Если для процесса нет никакого сигнала, процесс переходит в состояние ожидания до тех пор, пока не будет активизирован в результате поступления сигнала или удаления сообщения из очереди. После активизации процесса он снова пытается выполнить запись в очередь.
20190: Распределение достаточного пространства как для заголовка очереди сообщений (объект struct msg), так и для тела сообщения; как было упомянуто ранее, тело сообщения будет записано непосредственно после заголовка сообщения. Значение msg_spot заголовка будет указывать на место сразу после заголовка, где должно находиться тело сообщения.
20196: Копирует тело сообщения из пространства пользователя.
20202: Повторная проверка исправности очереди сообщений. Вход msgque мог быть изменен другим процессом в то время, когда процесс находился в состоянии ожидания после выполнения строки 20184, поэтому msq нельзя считать действительным указателем до тех пор, пока не будет выполнена его проверка.
Несмотря на это, создается впечатление, что здесь возможна ошибка. А что, если очередь сообщений будет уничтожена и по тому же индексу массива будет развернута другая очередь сообщений, прежде чем текущий процесс достигает этой точки? Это не может произойти на однопроцессорном компьютере, поскольку функция freeque, которая уничтожает очереди сообщений (строка 20440), активизирует все процессы, ждущие в очереди, перед ее уничтожением и не перейдет эту точку, пока не закончит свою работу функция real_msgsnd (функция freeque рассматривается далее в этой главе).
Однако все же, кажется, что риск этого на компьютерах с симметричной мультипроцессорной архитектурой невелик.
Если бы это произошло, значение msgque[id] не равнялось бы IPC_UNUSED или IPC_NOID, но память, на которую указывает msq, была бы освобождена функцией freeque, поэтому в строке 20203 происходила бы разадресация недействительного указателя.
20209: Соответственно, заполняет заголовок сообщения, ставит его в очередь и обновляет собственную статистику очереди (в том числе, общий размер сообщений). Обратите внимание, что работа по заполнению заголовка сообщения откладывалась до последней возможности, чтобы она не была сделана впустую, если бы возникла ошибка с момента распределения до текущего момента.
20226: Активизирует все процессы, которые могут ожидать поступления сообщения в эту очередь, а затем возвращает 0 в случае успеха.
20230: Функция real_msgrcv, аналогично real_msgsnd, реализует системный вызов msgrcv. Параметр msgtyp несет большую смысловую нагрузку, как описано в развернутом комментарии, который начинается в строке 20248. Именно здесь применяется поле msg_type объекта struct msg: в этой функции оно сравнивается с параметром msgtyp.
Функция real_msgrcv аналогична функции real_msgsnd также тем, что ее вызов происходит внутри пары lock_kernel/unlock_kernel в строке 20349.
20239: Извлекает индекс msgque из msgid и проверяет, что по этому индексу находится допустимый вход.
20253: Входит в цикл, который повторяется до тех пор, пока процесс не получит сообщение или не откажется от этих попыток и выполнит возврат. Несмотря на такую прозрачную структуру, этот цикл всегда разрывается в середине и это всегда происходит в результате возвращения из функции. Поэтому вместо него можно было бы применить цикл while (1); он действовал бы точно так же, но только немного быстрее.
20254: Проверка того, что процесс предоставил правильный порядковый номер и что он имеет разрешение читать из этой очереди.
20262: Эта последовательность if/else позволяет выбрать сообщение из очереди. Первый случай проще всего: он просто захватывает первое сообщение в очереди, если оно имеется, устанавливая значение nmsg либо равным NULL, либо равным указателю на первый элемент очереди.
20266: Параметр msgtyp положителен и бит MSG_EXCEPT (строка 15862) в структуре msgflg установлен. Функция real_msgrcv проходит по очереди, отыскивая первый вход, тип которого не соответствует msgtyp.
20272: Параметр msgtyp положителен, но бит MSG_EXCEPT не установлен. Функция real_msgrcv проходит по очереди, отыскивая первый вход, тип которого соответствует заданному.
20279: Параметр msgtyp отрицателен. Функция real_msgrcv отыскивает сообщение с наименьшим значением члена msg_type, если это значение также меньше абсолютного значения msgtyp. Обратите внимание, что поскольку в строке 20281 в качестве сравнения используется <, а не <=, предпочтение отдается первому сообщению в очереди. Это не только удобно (такое строгое соблюдение принципов организации последовательной очереди, по-видимому, вполне оправдано), но также чуть более эффективно, поскольку при этом приходится выполнять меньше операций присваивания. Если бы в качестве сравнения применялось <=, то появление каждого подходящего значения влекло бы за собой присваивание.
20287: Если к этому моменту какое-либо сообщение удовлетворяет заданным критериям, nmsg указывает на него. В ином случае, nmsg содержит NULL.
20288: Даже если было найдено подходящее сообщение, оно не обязательно будет возвращено. Например, если буфер вызывающей программы не позволяет разместить в нем все тело сообщения, вызывающая программа обычно получает ошибку E2BIG. Однако ошибка не выдается, если установлен бит MSG_NOERROR (строка 15860) структуры msgflg. (Трудно представить себе причину, по которой нужно было бы устанавливать в приложении флажок MSG_NOERROR, и такие приложения еще не встречались автору на практике.)
20292: Если параметр msgsz указывает больше байтов, чем существует в теле сообщения, функция real_msgrcv уменьшает параметр msgsz до размера сообщения. После этого msgsz равен числу байтов, которое должно быть скопировано в буфер вызывающей программы.
Более распространенный способ записи этого иногда немного медленнее, но в среднем, вероятно, может оказаться быстрее:
if (msgsz > nmsg->msg_ts) msgsz = nmsg->msg_ts;
20294: Удаляет выбранное сообщение из очереди. Очередь — это односвязный, а не двухсвязный список, поэтому, если удаляемое сообщение — не первое в очереди, функция real_msgrcv должна вначале пройти в цикле по очереди, чтобы найти предыдущий узел в очереди.
Нельзя обеспечить поиск предыдущего узла за постоянное время просто путем превращения очереди в двухсвязную.
Это изменение привело бы к потере места (для размещения дополнительных указателей), к потере времени (для обновления дополнительных указателей) и к потере простоты (из-за добавления кода, выполняющего эти действия). Тем не менее, эти потери не велики и двухсвязная организация очереди позволила бы существенно повысить скорость в том случае, когда удаляемое сообщение находится в середине очереди.
Однако на практике большинство приложений удаляют из очереди первое сообщение. В результате, дополнительное время, затраченное на управление указателями msg_prev (как их могли бы называть в двухсвязной очереди), обычно было бы явно потрачено напрасно. Оно окупалось бы только при удалении узла очереди из ее середины, но в приложениях это обычно не происходит.
Результатом явилось бы замедление в обычном случае ради ускорения в редком случае, а такое решение почти всегда является неудачным. Даже приложениям, в которых происходит удаление внутренних узлов очереди, не приходится ждать слишком долго, поскольку очереди сообщений обычно коротки и, как правило, не превышают нескольких десятков сообщений, и требуемое сообщение будет обнаружено в среднем примерно за половину итераций цикла. Следовательно, приложение может испытывать заметное замедление только в том случае, если очередь сообщений содержит сотни или тысячи сообщений, и приложение, как правило, удаляет внутренние узлы. Если учесть относительную маловероятность такого случая, разработчики ядра приняли наилучшее решение.
Кроме того, если приложение действительно характеризуется указанными особенностями и его разработчики крайне нуждаются в дополнительном повышении быстродействия, то для этого и предназначена система Linux. Разработчики приложения могут сами откорректировать исходный код ядра в соответствии со своими требованиями.
20305: Обеспечивает удаление единственного узла очереди.
20308: Обновляет статистику очереди сообщений.
20313: Активизирует все процессы, ожидающие записи к эту очередь сообщений, то есть все процессы, которые были переведены в состояние ожидания функцией real_msgsnd.
20314: Копирует сообщение в пространство пользователя и освобождает узел очереди (заголовок и тело).
20318: Выдает размер возвращаемого сообщения — это может быть важным для сообщений переменной длины, поскольку формат сообщения данного приложения может не предусматривать других способов определения конца сообщения.
20320: Ни одно сообщение не соответствовало критериям вызывающей программы. Дальнейшие действия зависят от вызывающей программы: если в вызывающей программе установлен бит IPC_NOWAIT структуры msgflg, то функция real_msgrcv может немедленно возвратить сообщение об отказе.
20323: В ином случае, вызывающая программа скорее перейдет в состояние ожидания, если нет доступных сообщений. Если появления ожидающего процесса ожидает какой-то сигнал, будет возвращена ошибка EINTR; иначе, вызывающая программа, вероятно, перейдет в состояние ожидания до тех пор, пока не поступит сигнал или пока какой-то другой процесс не начнет запись в очередь.
20329: Эта точка никогда не будет достигнута, но транслятор об этом не знает. Поэтому здесь находится фиктивный оператор return только для выполнения требований gcc.
20412: Поскольку структура управления функции sys_msgget проще по сравнению с sys_msgsnd и sys_msgrcv, нет необходимости переводить весь код функции sys_msgget в отдельную вспомогательную функцию. Однако она имеет свои собственные вспомогательные функции, которые рассматриваются далее в этой главе.
20414: Выполняется ненужная инициализация в –EPERM переменной ret, которая отслеживает требуемое возвращаемое значение функции. Переменная ret получает значение на каждом пути через функцию, поэтому присваивание в этой строке является избыточным. Однако оптимизатор транслятора gcc достаточно интеллектуален для того, чтобы устранить это ненужное присваивание, поэтому вопрос о том, действительно ли эта инициализация не нужна, остается открытым.
20418: Специальный ключ IPC_PRIVATE (код его объявления здесь не приведен, но его значение равно 0) говорит о том, что вызывающая программа требует создать новую очередь, независимо от того, существуют ли другие очереди сообщений с таким же ключом. В этом случае просто сразу же создается очередь с использованием функции newque (строка 20370), которая будет описана ниже.
20420: Иначе, ключ однозначно идентифицирует очередь сообщений, с которой хочет работать вызывающая программа. Обычно разработчик выбирает ключ более или менее произвольно (или предоставляет способ выбрать ключ пользователю) и надеется на то, что он не будет конфликтовать с ключами других работающих приложений.
Это утверждение может показаться преувеличенным, но имена временных файлов создают во многом аналогичную проблему — вам просто остается надеяться, что в других приложениях не будет выбрана такая же схема именования. Однако на практике эта проблема возникает редко: тип key_t с помощью typedef просто установлен в int, поэтому существует более 4-х миллиардов возможных значений на 32-разрядном компьютере и свыше 9 квинтиллионов — на 64-разрядном компьютере. Такой огромный размер пространства ключей позволяет уменьшить вероятность случайного столкновения. К тому же, схема прав доступа способствует дополнительному уменьшению вероятности возникновения проблем, даже если возникает случайное совпадение ключей очереди сообщений или имен файлов.
Однако нельзя ли применить лучший подход? Стандартные библиотечные функции С, такие как tmpnam, значительно упрощают выработку имен временных файлов, гарантируя их уникальность во всей системе, но эквивалентного способа выработки ключей очереди сообщений с гарантией их уникальности не существует.
При более внимательном изучении здесь обнаруживаются две разные проблемы. Для приложения обычно не важно, каким будет имя его временного файла, при условии, что оно не конфликтует с именем существующего файла. Но приложение, как правило, должно знать заранее, каким будет ключ его очереди сообщений, чтобы другие приложения, которые хотят послать в нее сообщения, знали, в какую очередь их посылать. Если приложение выбирает ключ очереди сообщений динамически, оно должно иметь способ сообщить другим приложениям этот выбранный ключ. (Оно может с таким же успехом передавать вместо ключа параметр msqid.) А если рассматриваемые приложения уже имеют способ посылать друг другу сообщения наподобие этого, то для чего им тогда нужны очереди сообщений?
Следовательно, эта проблема, вероятно, не стоит выеденного яйца. Приложение, которое требует получения уникального ключа для очереди общего пользования, но не накладывает слишком жестких ограничений на то, каким будет этот ключ, может получить его, просто попытавшись применить ключ 1 (помните, что 0 — это ключ IPC_PRIVATE), а затем пробуя последовательные значения ключа одно за другим, пока не добьется успеха — это потребует немного больше работы, но в действительности вряд ли понадобится.
Во всяком случае, в этой строке для поиска существующей очереди сообщений с данным ключом используется функция findkey (строка 20354; она рассматривается ниже).
20421: Если ключ не используется, sys_msgget может создать очередь. Если бит IPC_CREAT не установлен, возвращается ошибка ENOENT; в ином случае, функция newque (строка 20370) создает очередь.
20425: Ключ используется. Если в вызывающей программе установлены оба бита, IPC_CREAT и IPC_EXCL, то ей в этом случае нужно было получить ошибку и она ее получает. (Это было специально сделано в виде полного аналога битов O_CREAT и O_EXCL функции open.)
Кстати, трудно сказать, выполнялась бы проверка if быстрее, если бы она была записана либо в этой, либо в следующей эквивалентной форме:
} else if (msgflg & (IPC_CREAT | IPC_EXCL) == (IPC_CREAT | IPC_EXCL)) {
В обоих вариантах выполняется проверка того, установлены ли оба бита, но по разным причинам можно предполагать, что один из них будет работать быстрее другого. Однако на практике gcc вырабатывает одинаковый код для обоих вариантов, по крайней мере, при компиляции с оптимизацией. (Если вам это интересно, то оптимизатор выбирает вариант с прямолинейной трансляцией варианта, предложенного автором, то есть преобразует вариант, применяемый в ядре, в код, где проверка обоих битов происходит одновременно.) Это весьма впечатляющая оптимизация, на которую трудно было рассчитывать.
20428: В ином случае, ключ находится в использовании и вызывающая программа соглашается использовать существующую очередь с этим ключом. (Это наиболее распространенный случай.) Если в указанном месте нет очереди сообщений (а это никогда не должно произойти, с учетом реализации функции findkey) или вызывающая программа не имеет права обращаться к этой очереди, возвращается ошибка.
20434: В возвращаемом значении закодированы порядковый номер и индекс msgque. Оно становится параметром msgid, который вызывающая программа передаст функциям sys_msgsnd, sys_msgrcv и sys_msgctl.
Эта схема кодирования имеет две важных особенности. Более очевидная особенность состоит в том, как обеспечивается раздельное хранение части с порядковым номером и части с индексом массива: поскольку id — это индекс массива в msgque, он может только принимать значения, достигающие (но не включающие) MSGMNI, число элементов в msgque. Поэтому после умножения порядкового номера на это значение младшие биты остаются свободными для хранения id — это своего рода система счисления по основанию MSGMNI.
Отметим также, что возвращаемое значение никогда не может быть отрицательным — это важно, поскольку эта реализация библиотеки С может предусматривать интерпретацию возвращаемого отрицательного значения как ошибки. Поскольку значение MSGMNI в настоящее время установлено равным 128, индекс массива занимает младшие 7 битов возвращаемого значения. Порядковые номера занимают 16 битов, поэтому в результате этого присваивания в 1 могут быть установлены только младшие 23 бита переменной ret, а все старшие биты равны 0. Поэтому, в частности, знаковый разряд равен 0, так что переменная ret может быть только положительной или равной 0.
20437: Итак, вычисления выполнены и теперь возвращается значение ret.
20468: Можно смело утверждать, что sys_msgctl — самая большая функция в реализации очереди сообщений. Это отчасти связано с тем, что она выполняет множество различных действий, аналогично ioctl, и реализует набор слабо связанных функциональных средств. (Кстати, не обвиняйте в этой неразберихе разработчиков Linux; они просто обеспечили совместимость с уродливым проектом System V.) Параметр msqid содержит имя очереди сообщений, а cmd сообщает, что должна сделать с этой очередью функция sys_msgctl. Как вскоре станет очевидно, параметр buf может потребоваться или не потребоваться, в зависимости от cmd, и его назначение меняется от случая к случаю, даже когда он используется.
20477: Отбрасывает явно недопустимые параметры. Выполнение этого перед вызовом lock_kernel позволяет избежать ненужной блокировки ядра в том, по общему признанию, редком случае, когда эти параметры являются недопустимыми. (Безусловно, приходится соответствующим образом корректировать последовательность выполнения, поскольку в этом случае нужно также обойти выполнение unlock_kernel.)
20481: В случаях применения IPC_INFO и MSG_INFO вызывающая программа хочет получить информацию о свойствах данной реализации очереди сообщений. Она может, например, использовать эту информацию для выбора размера сообщений — на компьютерах с большими максимальными размерами сообщений вызывающий процесс может устанавливать свой собственный предел объема информации, отправляемой в расчете на одно сообщение.
Все явные константы, которые определяют применяемые по умолчанию пределы данной реализации очереди сообщений, копируются через объект типа struct msginfo (строка 15888). Включается небольшая дополнительная информация, если в качестве cmd применялась MSG_INFO, а не IPC_INFO, как показано, начиная со строки 20495, но во всем остальном эти два случая идентичны.
Обратите внимание, что буфер вызывающей программы, buf, был объявлен как указатель на объект другого типа, struct msqid_ds. Это не имеет значения. Копирование выполняется с помощью функции copy_to_user (строка 13735), для которой типы параметров не имеют значения, но она активизирует ошибку, получив запрос на запись в недоступную память. Если вызывающая программа предоставит указатель на достаточно большое пространство, функция sys_msgctl скопирует туда затребованные данные; за определение правильного типа (или хотя бы размера) отвечает вызывающая программа.
20505: Если копирование было выполнено успешно, функция sys_msgctl возвращает один дополнительный фрагмент информации, max_msqid. Обратите внимание, что в этом случае был полностью проигнорирован параметр msqid. Это абсолютно оправдано, поскольку предусматривает возврат информации о данной реализации очереди сообщений в целом, а не о какой-либо очереди сообщений в частности. Однако мнения о том, нужно ли в этом случае отбрасывать отрицательное значение msqid, могут расходиться. По общему признанию, отбросив недопустимые значения msqid, даже если эти значения не будут использоваться, можно, безусловно, значительно упростить код.
20508: Команда MSG_STAT запрашивает статистическую информацию о данной очереди сообщений, хранимую в ядре: ее текущий и максимальный размер, идентификаторы процессов ее самых последних программ чтения и записи и т.д.
20512: Возвращает ошибку, если параметр msqid является недопустимым, в заданной позиции не существует очередь или вызывающая программа не имеет разрешения читать из очереди. Поэтому разрешение на чтение из очереди означает разрешение читать не только сообщения, поставленные в очередь, но и «метаданные» о самой очереди.
Кстати, отметим, что команда MSG_STAT предполагает, что msqid включает только индекс msgque и не содержит порядкового номера.
20521: Вызывающая программа прошла все проверки. Функция sys_msgctl копирует затребованную информацию во временную переменную, а затем копирует временную переменную обратно через буфер вызывающей программы.
20533: Возвращает «полный» идентификатор, в котором теперь закодирован порядковый номер (это выполняется в строке 20520).
20535: Осталось три случая: IPC_SET, IPC_STAT и IPC_RMID. В отличие от рассмотренных ранее случаев, которые были полностью отработаны внутри этого переключателя, последние три выполняются здесь только частично. Первый из них, IPC_SET, предусматривает просто проверку того, что буфер, предоставленный пользователем, не равен NULL, a затем копирует его в переменную tbuf для последующей обработки далее в этой функции. (Отметим, что присваивание значения переменной err в строке 20540 вслед за копированием является лишним: переменной err будет снова присвоено значение в строке 20550 перед ее использованием.)
20542: Во втором из оставшихся трех случаях, IPC_STAT, просто выполняется проверка допустимости: настоящая работа для этой команды предусмотрена далее в этой функции. Для последнего из этих трех случаев, IPC_RMID, в этом переключателе работы нет; вся его работа отложена в этой функции на дальнейшее.
20548: Этот код является общим для всех оставшихся случаев и теперь он должен выглядеть для вас знакомым: в нем происходит извлечение индекса массива из msqid, проверка того, что по указанному индексу существует действительная очередь сообщений и сверка порядкового номера.
20559: Выполнение оставшейся части команды IPC_STAT. Если пользователь имеет разрешение читать из очереди, функция sys_msgctl копирует статистическую информацию в буфер вызывающей программы. Если вам кажется, что это в значительной степени напоминает основную часть описанного раннее случая MSG_STAT, то вы правы. Единственным различием между этими двумя случаями является то, что MSG_STAT ожидает «неполный» msqid, как было показано выше, a IPC_STAT ожидает «полный» (то есть включающий порядковый номер).
20572: Копирует статистические данные в пространство пользователя. Эти три строки выполнялись бы чуть быстрее, если бы были перезаписаны следующим образом:
err = 0 ; if (copy_to_user(buf, &tbuf, sizeof(*buf))) err = -EFAULT;
В конце концов, запись в пространство пользователя, безусловно, чаще оканчивается успехом, чем неудачей. По той же причине, соответствующий код в случае MSG_STAT (начиная со строки 20530) работал бы быстрее, если его перезаписать следующим образом:
if (copy_to_user(buf, &tbuf, sizeof(*buf))) { err = -EFAULT; goto out; } err = id;
Или даже могли бы работать быстрее два следующих варианта, поскольку в них не выполняется ненужное присваивание:
if (copy_to_user (buf, &tbuf, sizeof(*buf))) err = -EFAULT; else err = 0;
или
err = copy_to_user (buf, &buf, sizeof(*buf)) ? -EFAULT : 0 ;
Вопреки очевидному, проведенные автором проверки всех этих изменений показывают, что версия ядра работает быстрее. Причина этого заключается в том, как gcc вырабатывает объектный код: по-видимому, стоимость дополнительного присваивания в версии ядра не идет в сравнение со стоимостью дополнительного перехода в версии автора. (Дополнительный переход нельзя сразу обнаружить, рассматривая исходный код С: необходимо просматривать ассемблерный вывод gcc.) Напомним, что в предыдущих главах мы уже говорили о том, что переходы связаны со значительными потерями, поскольку они заставляют процессор терять преимущества некоторых внутренних алгоритмов параллельного выполнения. Разработчики процессора сделали очень многое по предотвращению влияния потерь, связанных с ветвлениями, но, очевидно, не смогли устранить все проблемы.
В конечном итоге, в процессе дальнейшего усовершенствования оптимизатора gcc разница между версией ядра и версией автора может стать более очевидной. Если две формы логически эквиваленты и одна из них быстрее, было бы замечательно, если бы gcc мог обнаруживать их эквивалентность и вырабатывать одинаковый код для них обоих. Однако эта проблема сложнее, чем может показаться. Для выработки самого быстрого кода gcc должен обладать способностью предвидеть, какое присваивание будет происходить с наибольшей вероятностью — другой случай предусматривает ветвление. (Однако в процессе работы над новейшими версиями gcc были заложены основы для таких усовершенствований.)
20576: В случае IPC_SET вызывающая программа стремится установить некоторые параметры очереди сообщений: ее максимальный размер, владельца и режим.
20578: Для того, чтобы иметь возможность манипулировать с параметрами очереди сообщений, вызывающая программа должна либо владеть этой очередью, либо иметь возможности CAP_SYS_ADMIN (строка 14092). Возможности описаны в главе 7.
20584: Повышение предела максимального числа байтов в очереди сообщений свыше обычного максимума во многом аналогично повышению максимальных значений любого другого ресурса свыше его жестко закодированного ограничения, поэтому для этого повышения требуется такая же возможность, CAP_SYS_RESOURCE (строка 14117). Ограничения ресурсов рассматриваются в главе 7.
20587: Вызывающей программе нужно позволить выполнить эту операцию, поэтому выбранные параметры установлены из буфера tbuf, предоставленного вызывающей программой.
20595: Команда IPC_RMID означает удаление указанной очереди: не только сообщений в ней, но и самой очереди. Если вызывающая программа владеет очередью или имеет возможность CAP_SYS_ADMIN, очередь освобождается с помощью вызова функции freeque (строка 20440).
20605: В конце концов, cmd не оказалась одной из распознанных команд, поэтому вызывающая программа получает ошибку EINVAL. В таком случае, можно было бы избежать работы, выполненной в строке 20548. Предположим, что мы попытались бы обнаружить неправильное значение cmd при самой первой возможности, удалив случай default из переключателя и добавив следующий код в первый переключатель функции в строке 20546:
case IPC_RMID: break; /* Еще нечего делать. */ default: err = -EINVAL; goto out; break; /* He достигнуто. */
Это привело бы к изменению поведения функции. Если бы вызывающая программа предоставила недопустимое значение и cmd, и msqid, она получила бы ошибку, отличную от той, которую получает сейчас: после этого изменения недопустимое значение cmd было бы обнаружено раньше недопустимого значения msqid. Однако документация к msgctl не обещает ни той, ни иной реакции, поэтому мы должны быть вправе внести это изменение. Результатом явилось бы незначительное ускорение случая с недопустимым значением cmd.
Однако, отметим, что это решение, к сожалению, требует введения пустого случая IPC_RMID в первом переключателе. Без этого функция неправильно отбросила бы IPC_RMID как неверное значение cmd. Эта дополнительная ветка case замедляет обработку, не намного, но замедляет, при нормальных условиях, в которых cmd имеет допустимое значение. К тому же, как вы знаете, ускорение выполнения редкого случая за счет обычного почти никогда не бывает хорошим решением. Поэтому лучше оставить все, как еcть.
20354: Функция findkey находит очередь сообщений с данным ключом от имени функции sys_msgget (вызов находится в строке 20420).
20359: Начинается цикл по всем слотам, которые могут заняты в msgque. Значение max_msqid позволяет отслеживать наивысший занятый элемент массива в msgque; оно используется здесь и сопровождается с помощью функций newque и freeque, которые вскоре будут рассмотрены. Без параметра max_msqid пришлось бы проходить в этом цикле по всем MSGMNI (128) элементам msgque, даже если, скажем, используются первые 5.
20360: Если текущий элемент массива имеет значение IPC_NOID, то здесь создается очередь сообщений. Очередь сообщений может иметь искомый ключ, поэтому функция findkey ожидает полного создания очереди. (Она может перейти в это состояние, когда вызов функции kmalloc в строке 20385 переводит процесс в состояние ожидания.)
20362: Если данный вход msgque не используется, ясно, что он не будет иметь соответствующего ключа.
20364: Если совпадающий ключ найден, будет возвращен соответствующий индекс массива.
20367: Если цикл был выполнен до конца и не найден соответствующий ключ, возвращается –1 в качестве сигнала об отказе.
20370: Функция newque ищет неиспользуемый вход msgque и пытается создать здесь новую очередь сообщений.
20376: Проходит в цикле через msgque в поиске неиспользуемого входа. Если таковой будет найден, он будет отмечен значением IPC_NOID и управление перейдет на метку found в строке 20383.
20381: Если цикл завершится без обнаружения неиспользуемого входа, это значит, что массив msgque заполнен. Функция newque возвращает ошибку ENOSPC, которая указывает на то, что в таблице не осталось места.
20384: Распределяет объект struct msqid_ds, который будет представлять новую очередь.
20387: Если распределение окончится неудачей, этот вход msgque снова устанавливается в IPC_UNUSED.
20388: Активизируются все процессы findkey, которые были переведены в состояние ожидания после встречи IPC_NOID.
20391: Инициализация новой очереди.
20404: Если эта очередь была сформирована вслед за наивысшим ранее используемым слотом в msgque, функция newque соответствующим образом увеличивает параметр max_msqid.
20406: Устанавливает новую очередь в msgque.
20408: Активизация всех процессов findkey, которые могли ожидать окончания инициализации этой очереди.
20409: Возвращает порядковый номер и индекс массива msgque. (He мешало бы создать небольшой набор макрокоманд для выполнения этого кодирования и последующего декодирования.) Как ни странно, порядковый номер здесь не наращивается; это происходит в функции freeque, которая рассматривается ниже. Если подумать, это решение имеет смысл. Вам не нужен уникальный порядковый номер для каждой очереди — вам нужен только другой порядковый номер при каждом повторном использовании элемента msgque, чтобы данное сочетание индекса массива и порядкового номера больше не могло повториться. Индекс массива не может быть повторно использован до тех пор, пока очередь, установленная здесь, не будет освобождена, поэтому наращивание порядкового номера можно отложить до этого момента.
Просто для того, чтобы подчеркнуть правильность такого решения, отметим, что предусмотрена возможность применения одинакового порядкового номера двумя элементами msgque одновременно.
20440: Описание функции freeque, которая удаляет очередь и освобождает соответствующий вход msgque, позволяет подвести черту под обсуждением данной реализации очереди сообщений ядра.
20449: Если освобождаемый вход msgque представляет собой используемый вход с наибольшим индексом, функция freeque уменьшает значение max_msqid в наибольшей возможной степени. После этого цикла max_msqid снова представляет собой индекс максимального используемого входа msgque или 0, если все входы являются неиспользуемыми. Обратите внимание, что если max_msqid имеет значение 0, то массив msgque либо пуст, либо содержит только один вход.
20452: Элемент массива msgque отмечен как неиспользуемый, хотя объект struct msqid_ds еще не освобожден (функция freeque все еще содержит указатель на объект struct msqid_ds в переменной msq).
20454: Если какие-либо процессы ожидают чтения или записи в эту очередь, их нужно предупредить о том, что очередь вскоре исчезнет. В этом цикле все они активизируются. Все процессы, ожидающие отправки сообщения в очередь, обнаружат изменившийся порядковый номер в строке 20171; все процессы, ожидающие получения сообщения из очереди, обнаружат это же в строке 20254.
20458: Вызов функции schedule (строка 26686, описанной в главе 7) для предоставления активизированным процессам шанса на выполнение. Интересно, что активизированные процессы могут пока не получить процессорного времени, поскольку текущий процесс может иметь более низкое значение параметра goodness (более высокий приоритет). Однако, если это произойдет, то вновь активизированные процессы просто не будут удалены из соответствующих им очередей ожидания; функция freeque обнаружит это, продолжая проходить по циклу, и попытается снова активизировать процессы. В конечном итоге, процесс, выполняющий функцию freeque, исчерпает отведенное ему время и уступит место другим процессам. Тем не менее, с учетом всех обстоятельств, может быть лучше явно устанавливать флажок SCHED_YIELD текущего процесса (строка 16202) до того, как он вызовет schedule, чтобы предоставить другим процессам лучшие условия доступа к процессору.
20460: Нет ожидающих процессов чтения или записи, поэтому можно безопасно освободить очередь и все ее сообщения.
netlib.narod.ru | < Назад | Оглавление | Далее > |