Стратегия оценки / вычисления буферного пространства, необходимого функции записи во встроенной системе

#c #buffer

#c #буфер

Вопрос:

Это не проблема программирования, останавливающая показ, как таковая, но, возможно, скорее проблема шаблона проектирования. Я бы подумал, что это будет обычная проблема проектирования во встроенных системах с ограниченными ресурсами, но ни один из вопросов, которые я нашел до сих пор в SO, не кажется актуальным (но, пожалуйста, укажите что-нибудь важное, что я мог пропустить).

По сути, я пытаюсь разработать наилучшую стратегию оценки наибольшего размера буфера, требуемого некоторой функцией записи, когда выходные данные этой функции записи не фиксированы, особенно потому, что некоторые данные представляют собой текстовые строки переменной длины.

Это приложение на языке Си, которое работает на небольшом ARM micro. Приложению необходимо отправлять различные типы сообщений через сокет TCP. Когда я хочу отправить TCP-пакет, стек TCP (Keil RL) предоставляет мне буфер (который библиотека выделяет из своего собственного пула), в который я могу записать полезную нагрузку пакетных данных. Этот размер буфера, конечно, зависит от MSS; поэтому давайте предположим, что это максимум 1460, но может быть меньше.

Как только у меня есть этот буфер, я передаю этот буфер и его длину функции записи, которая, в свою очередь, может вызывать различные вложенные функции записи для построения полного сообщения. Причина такой структуры в том, что я фактически создаю небольшой XML-документ, где каждая функция записи обычно генерирует определенный элемент XML. Каждая функция записи хочет записать некоторое количество байтов в мой выделенный буфер пакетов TCP. Я точно знаю только, сколько байтов данная функция записи записывает во время выполнения, потому что часть инкапсулированного содержимого зависит от определяемых пользователем текстовых строк переменной длины.

Некоторые сообщения должны иметь размер около (скажем) 2K, что означает, что они, вероятно, будут разделены по крайней мере на две операции отправки пакетов TCP. Эти сообщения будут создаваться путем вызова ряда функций записи, которые производят, скажем, сто байт за раз.

Перед выполнением вызова каждой функции записи или, возможно, внутри самой функции записи, мне сначала нужно сравнить доступное буферное пространство с тем, сколько требуется для этой функции записи; и если свободного места недостаточно, затем передайте этот пакет и продолжите запись в новый пакет позже.

Возможные решения, которые я рассматриваю::

  1. Используйте другой гораздо больший буфер для первоначальной записи всего. Это не является предпочтительным из-за ограничений ресурсов. Кроме того, я все равно хотел бы иметь средство для алгоритмического определения того, сколько места мне нужно для моих функций записи сообщений.

  2. Во время компиляции создайте константу «наихудшего размера» для каждой функции записи. Каждая функция записи обычно генерирует элемент XML, такой как <START_TAG>[string]</START_TAG> , поэтому у меня могло бы быть что-то вроде: #define SPACE_NEEDED ( START_TAG_LENGTH START_TAG_LENGTH MAX_STRING_LENGTH SOME_MARGIN ) . Все мои функции записи содержимого в любом случае выбираются из таблицы указателей функций, поэтому я мог бы иметь константы оценки размера в наихудшем случае для каждой функции записи в виде нового столбца в этой таблице. Во время выполнения я проверяю буферную комнату на соответствие этой оценочной константе. Это, вероятно, мое любимое решение на данный момент. Единственным недостатком является то, что для его работы требуется правильное обслуживание.

  3. Мои функции записи обеспечивают специальный режим «фиктивного запуска», в котором они запускаются и вычисляют, сколько байтов они хотят записать, но ничего не записывают. Этого можно было бы достичь, возможно, просто отправив NULL вместо указателя буфера на функцию, и в этом случае возвращаемое значение функции (которое обычно указывает количество, записанное в буфер) просто указывает, сколько он хочет записать. Единственное, что мне в этом не нравится, это то, что между «фиктивным» и «реальным» вызовом базовые данные могут — по крайней мере, в теории — измениться. Возможным решением для этого может быть статический сбор базовых данных.

Заранее спасибо за любые мысли и комментарии.

Решение

То, что я на самом деле уже начал делать с момента публикации вопроса, заключалось в том, чтобы заставить каждую функцию записи содержимого принимать параметр состояния или «итерации», который позволяет функции записи многократно вызываться функцией отправки TCP. Writer вызывается до тех пор, пока не отметит, что ему больше нечего записывать. Если функция отправки TCP решает после определенной итерации, что буфер теперь почти заполнен, она отправляет пакет, а затем процесс продолжается позже с новым буфером пакетов. Я думаю, что этот метод очень похож на ответ Макса, который я поэтому принял.

Ключевым моментом является то, что на каждой итерации средство записи содержимого должно быть спроектировано так, чтобы оно не записывало в буфер больше LENGTH байтов; и после каждого вызова средства записи функция отправки TCP проверяет, осталось ли у LENGTH нее место в буфере пакетов, прежде чем снова вызывать средство записи. Если нет, это продолжается в новом пакете.

Еще одним шагом, который я сделал, было серьезно подумать о том, как я структурирую заголовки своих сообщений. Стало очевидно, что, как я полагаю, почти со всеми протоколами, использующими TCP, важно внедрить в протокол приложения некоторые средства указания общей длины сообщения. Причина этого в том, что TCP является потоковым протоколом, а не пакетным протоколом. Это снова вызвало небольшую головную боль, потому что мне нужны были некоторые предварительные средства для определения общей длины сообщения для вставки в начальный заголовок. Простым решением этого было вставить заголовок сообщения в начало каждого отправленного TCP-пакета, а не только в начало сообщения протокола приложения (которое, конечно, может охватывать несколько TCP-сокетов), и в основном реализовать фрагментацию. Итак, в заголовке я реализовал два флага: fragment флаг и last-fragment флаг. Поэтому поле длины в каждом заголовке должно указывать только размер полезной нагрузки в конкретном пакете. На принимающей стороне отдельные фрагменты заголовка полезной нагрузки считываются из потока, а затем повторно собираются в полное сообщение протокола.

Это, конечно, без сомнения, очень упрощенно, как HTTP и многие другие протоколы работают поверх TCP. Просто довольно интересно, что только после того, как я попытался написать надежный протокол, работающий через TCP, я начал понимать важность действительно продумывания структуры вашего сообщения с точки зрения заголовков, фрейминга и т. Д., Чтобы оно работало по потоковому протоколу.

Ответ №1:

У меня была связанная с этим проблема в гораздо меньшей встроенной системе, работающей на микроконтроллере PIC 16 (и написанной на языке ассемблера, а не на C). Мой «размер буфера» всегда был двухбайтовой очередью передачи UART, и у меня была только одна функция записи, которая выполняла DOM и передавала его XML-сериализацию.

Решение, которое я придумал, состояло в том, чтобы вывернуть проблему «наизнанку». Функция записи становится задачей: каждый раз, когда она вызывается, она записывает столько байтов, сколько может (что может быть> 2 в зависимости от скорости последовательной передачи данных), пока буфер передачи не заполнится, затем он возвращается. Однако в переменной состояния он запоминает, как далеко он прошел через DOM. При следующем вызове он запускается с ранее достигнутой точки. Задача записи вызывается повторно из цикла. Если свободного буферного пространства нет, оно немедленно возвращается без изменения его состояния. Он вызывается повторно из бесконечного цикла, который действует как планировщик циклического перебора для этой задачи и других в системе. Каждый раз в цикле возникает задержка, которая ожидает переполнения таймера TMR0. Таким образом, каждая задача вызывается ровно один раз за фиксированный временной интервал.

В моей реализации данные передаются с помощью процедуры прерывания TxEmpty, но они также могут быть отправлены другой задачей.

Я предполагаю, что «шаблон» здесь заключается в том, что одна из ролей счетчика программ заключается в сохранении текущего состояния потока управления, и что эта роль может быть перенесена с ПК на другую структуру данных.

Очевидно, что это не сразу применимо к вашей более крупной системе более высокого уровня. Но это другой взгляд на проблему, который может вызвать ваше собственное понимание.

Удачи!

Комментарии:

1. Привет, Макс, большое спасибо за ваш любезный ответ и извините, что мне потребовалось так много времени, чтобы правильно вернуться к этому! (Раньше я сам много работал с ассемблером PIC — в основном C в наши дни.) Я отредактирую свой исходный текст вопроса с тем, что я сделал в конце.