Переполнение String?

andrey14
Offline
Зарегистрирован: 19.04.2015

Здравствуйте!

 

Делаю маленький вебсервер. Столкнулся с неожиданной для меня проблемой, собирая Html-страницу из десятка переменных для передачи ее из Arduino Nano по UART, при запросе страницы в браузере, ее содержание не соответствовало ожиданиям. Html документ формировался суммированием строк:

1String Content;
2Content= "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n";
3Content+=String(var1)+"<br>\r\n";
4Content+=String(var2)+"<br>\r\n";
5Content+=String(var3)+"<br>\r\n";
6...
7Content+=String(var15)+"<br>\r\n";

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

Проблему решил отправляя по UART не всю страницу сразу в переменной "Content", а отправляя данные частями без собирания в общий пакет (в переменную).

Я так понял, что не хватало памяти, и оно писало абы куда, и на выходе получалось абы что.

Остался теперь вопрос: как узнать максимально возможную длину переменной типа String? До каких пор можно удлинять переменную Content? Может есть какие то способы увидеть, что что то пошло не так?

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Пока в оперативку помещается - всё ок, дальше - будут глюки. Отсюда вывод: максимальная длина в байтах, которую можно впихнуть в переменную типа String - это кол-во свободной оперативной памяти.

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

String - это не переменная, это класс. http://arduino.ru/Reference/StringObject

andrey14
Offline
Зарегистрирован: 19.04.2015

После компиляции пишет:

Скетч использует 25 828 байт (84%) памяти устройства. Всего доступно 30 720 байт.
Глобальные переменные используют 1 351 байт (65%) динамической памяти, оставляя 697 байт для локальных переменных. Максимум: 2 048 байт.

Как контролировать эту свободную память? А если объявлено несколько переменных? Как однозначно узнать или просчитать грядущий косяк?

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

Кстати, в HTML \r\n совершенно не нужны.

Где у Вас объявлена переменная Content? Внутри функции?

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

andrey14 пишет:

После компиляции пишет:

Скетч использует 25 828 байт (84%) памяти устройства. Всего доступно 30 720 байт.
Глобальные переменные используют 1 351 байт (65%) динамической памяти, оставляя 697 байт для локальных переменных. Максимум: 2 048 байт.

Как контролировать эту свободную память? А если объявлено несколько переменных? Как однозначно узнать или просчитать грядущий косяк?

В 328 меге всего 2кб памяти - не каждая страница вместится в такой размер. :)

andrey14
Offline
Зарегистрирован: 19.04.2015

faeton пишет:

Где у Вас объявлена переменная Content? Внутри функции?

Да внутри.

Да там и небольшая Хтмл - 10 строк по 15-20 символов.

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

andrey14 пишет:

faeton пишет:

Где у Вас объявлена переменная Content? Внутри функции?

Да внутри.

Да там и небольшая Хтмл - 10 строк по 15-20 символов.

Локальная переменная, объявленная внутри процедуры, хранится в стеке. Что делает дунин компилятор со стеком: сколько его выделяется и как он контролируется я достоверно не знаю, знающие люди подскажут.

200 символов влезут даже в короткую строку. Но не забывайте, что Вы в МК на нищем пайке и тут транжирить память, котрой менее 2-х кБ,  налево-направо, склеивать строки для красоты в одну не всегда прокатит. Увеличение длинны строки вообще не простая процедура, да и вообще, строки с завершающим нулём чертовки неудобная штука. Думаю, Вам совершенно не критично за четверь или треть секунды строка будет отдана в порт, потому неизменный заголовок объявляйте как строковую константу и она ляжет в флэш, которой побольще, чем ram, переменные отдавайте по одной.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

faeton пишет:

Где у Вас объявлена переменная Content? Внутри функции?

Это пофиг. String хапает память из кучи malloc'ом

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Угу. А "свободной" памяти под malloc() И стек компилятор говорит осталось .. 697 байт .. ВСЕГО. Да, ещё как понимаю у вас utf-8 по сколько там "байт на символ"? А ещё конкатенация строк приводит к realloc() тем или иным способом, и столько раз, сколько конкатенаций имеется .. то бишь имеем ещё и фрагментацию кучи .. а дуньковский malloc() точно умеет дефрагментировать кучу? В какой момент времени? .. :)

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

И вот оно снова .. то самое безграмотное "классовое" копро-программирование, которое вредно для микроконтроллеров. Так сказать "воочию". А ведь 2 кила оперативы более чем достаточно чтобы сварганить и отправить сраницу, описанную автором темы! :)

В целом, С++ сильный и крайне мощный язык и изучать его безусловно надо и на очень высоком уровне .. хотя бы для того, чтобы понять что на практике, для программирования микроконтроллеров он НАФИГ НЕ НУЖЕН. :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Некоторые это благополучно понимают и без изучения.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да, но тем более странно, как эти "некоторые" впику такого понимания НАВЯЗЫВАЮТ С++ начинающим ардуинщикам.

"Копро-программирование - в действии" :)

andrey14
Offline
Зарегистрирован: 19.04.2015

faeton пишет:

Но не забывайте, что Вы в МК на нищем пайке и тут транжирить память, котрой менее 2-х кБ,  налево-направо, склеивать строки для красоты в одну не всегда прокатит. Увеличение длинны строки вообще не простая процедура,

Так какой же выход у ардуинщика любителя в такой ситуации? Как гарантированно безболезненно работать с увеличением строки? Объявлять массив символов или есть еще варианты?

Я сейчас решил вопрос так:

1  ESPport.print("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n");
2  ESPport.print(String(temp1,4));
3  ESPport.print(String(avertemp1/count,4));
4...

сразу на передачу. Но теоретически и тут String может не поместиться в оставшуюся память...

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

andrey14 пишет:

Может кто ответит: если невозможно контролировать оставшуюся память, 

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

andrey14 пишет:

Так какой же выход у ардуинщика любителя в такой ситуации? Как гарантированно безболезненно работать с увеличением строки? Объявлять массив символов или есть еще варианты?

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

Но, для начала, надо просто более аккуратно программировать. Вот смотрите, что Вы тут наваяли (это я ещё всего кода не видел!):
 
1String Content;
2Content= "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n";
3Content+=String(var1)+"<br>\r\n";
4Content+=String(var2)+"<br>\r\n";
5Content+=String(var3)+"<br>\r\n";
6...
7Content+=String(var15)+"<br>\r\n";

В строке 2 Вы выделяете память точно под символьную константу, хотя знаете. что строка будет расти, но не заботитесь об этом!

В строке 3 Вы освободжаете память, выделенную в строке 2 и запрашиваете новую. Но, поскольку новый кусок больше старого, старый остаётся неиспользованным и хотя формально он свободен, память он продолжает занимать. Если бы Вы хотя бы не использовали там "String(var1)" непонятно зачем, то старый кусок памяти скорее всего бы повторно использовался (точно не уверен, надо смотреть текст malloc, но обычно если кусок с краю, то realloc просто удлиняет его). Но, поскольку Вы использовали "String(var1)", Вы подпёрли старый кусок памяти с хвоста и не дали ему возможности расшириться - заставили менеджер памяти выделять новый кусок.

В строке 4 Вы опять проделываете тоже самое, т.е. хватаете новый кусок памяти, оставив старый просто болтаться и занимать ресурсы.

В строке 5 Вы опять заправшиваете новый кусок памяти и опять старый оставляете занимать место (у Вас уже три куска просто занимают место и их память никак не используется!!!)

И так в каждой строке.

Написать более расточительно по памяти просто очень трудно (хотя, специалисты, конечно, имеются).

 

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

andrey14 пишет:
Так какой же выход у ардуинщика любителя в такой ситуации? Как гарантированно безболезненно работать с увеличением строки?

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

Вам уже и я отписал, и ЕвгенийП мой пост "разжевал" .. не надо "работать с увеличением строки" .. и ВООБЩЕ с классами работать НЕ надо!

Все строки загоняются во флеш и достаются оттудова по мере необходимости и сразу, то бишь поштучно, отправляются в ответ. Если есть возможность, то надо запихать во флеш уже готовые ответы ЦЕЛИКОМ и оттадавать их оттуда не расходуя ни одного байта оперативы..

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

andrey14,

ещё к вопросу об аккуратном программировании работы с памятью (я уже писал, что у Вас написано крайне неаккуратно). Для аккуратной работы в первом приближении не нужно ни каких-то особых знаний, ни выдающегося ума. Нужно просто аккуратно "прибирать за собой", т.е. не оставлять в памяти более ненужные переменные. Так ли это сложно?

Вот, давайте проведём эесперимент по загаживанию памяти экземплярами класса String.

Для начала запустите такой скетч:

Как видите. он по идее ложен печатать 10 строк. Но печатает на моей Nano всего четыре (если у Вас Мега - напечатает больше, но всё равно немного).

Почему? Да всё просто. Все переменные, которые уже давно отпечатаны и болеее не нужны, по-прежнему торчат в памяти. Зачем?

Давайте изменим этот скетч всего чуть-чуть, а именно определения строк и их печать заключим в фигурные скобки, чтобы память освобождалась по достижении закрывающей скобки. Вот так:

и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!

andrey14
Offline
Зарегистрирован: 19.04.2015

Я уже понял в чем косяк, поэтому уже и переписал несколько по-другому.

Использовал String(var1) так как var1 и остальные var'ы - float.

А на счет профессионалов, думаю они не маются Ардуино, и не будут в конце проекта впихивать в оставшиеся 5 килобайт менеджер памяти, или может я сильно далек от этого... Статью по вашей ссылке переварить кусок времени надо ;)

ЗЫ Других вариантов (полупрофессиональных) нет? ;)

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

andrey14 пишет:

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

Сильно далеки :)))

andrey14 пишет:

ЗЫ Других вариантов (полупрофессиональных) нет? ;)

Всегда есть 100500 спосроов что-то сделать, на надо задачу знать. А Вы там ещё и флоат пользуете - четверть всей памяти сразу коту под то самое место ...

А про освобождение памяти, помотрите мой пример из предыдущего поста. Не ахти что, но приёмчик действенный, как видите.

 

 

andrey14
Offline
Зарегистрирован: 19.04.2015

ЕвгенийП пишет:

и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!

Я в легком шоке ;) Сам бы точно до такого не дошел. Как бы вообще о скобках не задумывался...

andrey14
Offline
Зарегистрирован: 19.04.2015

ЕвгенийП пишет:

А Вы там ещё и флоат пользуете - четверть всей памяти сразу коту под то самое место ...

Что выдают библиотеки, то и использую ;)

Раньше в CVAVR занимал свое время. Там все своими руками напишешь, и все более менее прозрачно. А потом открыл для себя ардуино и поддался на простоту - вставил, подключил, заработало - даунгрейд получается ;)  А оказывается нифига местами не проще. А почему глючит - непонятно: или библиотека кривая - лезешь правишь, или сам нахомутал, или вот такое чего еще не знаю.

Ну и на этом спасибо. Буду теперь немного по другому смотреть на память.

rapidshe
Offline
Зарегистрирован: 31.12.2015

ЕвгенийП пишет:

и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!

взрыв мозга. слов нет.... можете объяснить это? почему такую глобальную роль играет обособление в скобки?

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

rapidshe пишет:

ЕвгенийП пишет:

и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!

взрыв мозга. слов нет.... можете объяснить это? почему такую глобальную роль играет обособление в скобки?

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

rapidshe пишет:

можете объяснить это? 

Ну, кратко коллега уже объяснил. А подробно, я скоро выложу третью серию своего эссе про использование памяти. Там это будет одним из двух центральных вопросов.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

faeton пишет:

Кстати, в HTML \r\n совершенно не нужны.

Зачем же вы ерунду говорите ? В хидерах без них никак ! В теле - не нужны.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

И что скажут знатоки... Происходит ли освобождение памяти при "укорачивании" стринга, скажем присвоением ему пустой строки или тупо установкой нулевого размера ? ;)

А вообще для любителей юзать длинные текстовые строки имеет смысл посмотреть макросы F() и PSTR() и соответствующие функции, скажем strcpy_p() и уйму таких же других с _p на конце.

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

brokly пишет:

faeton пишет:

Кстати, в HTML \r\n совершенно не нужны.

Зачем же вы ерунду говорите ? В хидерах без них никак ! В теле - не нужны.

Не путайте http и html. :)

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

А... понятно, а вы типа так никого не запутали ?

faeton
faeton аватар
Offline
Зарегистрирован: 21.03.2016

brokly пишет:

А... понятно, а вы типа так никого не запутали ?

Я написал совершенно локанично и точно: в html не нужны. В чём вопрос? Зачем Вы приплели сюда заголовок http запроса? Котлетки отдельно! :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

brokly пишет:

И что скажут знатоки... 

Посоветуют заглянуть в файл 

<корень IDE>\hardware\arduino\avr\cores\arduino\WString.cpp

Там всё написано.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

faeton пишет:

Я написал совершенно локанично и точно: в html не нужны. В чём вопрос? Зачем Вы приплели сюда заголовок http запроса? Котлетки отдельно! :)

Может не запроса, а ответа ? Ну если уж мух от котлет отделять.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

ЕвгенийП пишет:

Там всё написано.

Мда... знатоки оказались не знатоками :) Да мне и ответ был не нужен. Естественно память освобождается, по крайней мере в той версии которую использую я.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

brokly пишет:

Естественно память освобождается,

Совершенно неестественно. Как Вы выразились "при "укорачивании" стринга", скажем присвоением ему пустой строки или тупо установкой нулевого размера" обычно ничего не освобождается.

brokly пишет:

по крайней мере в той версии которую использую я.

Что ж за версию Вы такую используете? Сами написали или с IDE такая пришла? Если с IDE, то что за версия? В тех IDE, что я видел, сделано классически (так же как в MFC и во многих других реализациях) а именно - с памятью в таких случаях никаких манипуляций вообще не производится.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Теперь настала моя очередь отправить вас в <корень IDE>\hardware\arduino\avr\cores\arduino\WString.cpp :)

Идите и смотрите :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

brokly пишет:

Идите и смотрите :)

Именно это я и собирался сделать, когда спросил у Вас какая у Вас версия IDE, чтобы мы смотрели одно и тоже. Секрет?

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

А я посмотрел уже достаточно версий , везде одно и то-же. При присвоении пустой строки стрингу память полностью освобождается :) Даже у ESPшки, не говоря уже о настоящих ардуинах :)

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

Ну давайте глянем что у вас там написано. У меня вот:

1String & String::operator = (const String &rhs)
2{
3    if (this == &rhs) return *this;
4     
5    if (rhs.buffer) copy(rhs.buffer, rhs.len);
6    else invalidate();
7     
8    return *this;
9}

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Чёт я не понял, что значит "пустому стрингу"?

Вы писали: "Происходит ли освобождение памяти при "укорачивании" стринга, скажем присвоением ему пустой строки". Так всё-таки "при укорачивании" или при "присваваивании пустой строке пустую же"?

Вот такая конструкция:

1String s = "Seychas my ee ukorotim";
2// Укорачиваем
3s = "";

Это то случай или не тот? Освобождается при таком присваивании память или нет?

Кстати, если это "не тот случай", приведите, пожалуйста,  пример присваивания более короткой строки более длинной (укорачивания), которое приведёт к освобождению ранее занимаемой памяти.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Тот случай.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

И вот в этом случае, по Вашему, освобождается память?

Ну, что ж, давайте посмотрим. Начнём с самого оператора

1String & String::operator = (const char *cstr)
2{
3    if (cstr) copy(cstr, strlen(cstr));
4    else invalidate();
5    return *this;
6}

Итак, если переданный параметр не NULL, то он просто вызывает copy и после этого возвращает себя.

Хорошо, смотрим, что там за copy. Вот он родимый:

01String & String::copy(const char *cstr, unsigned int length)
02{
03    if (!reserve(length)) {
04        invalidate();
05        return *this;
06    }
07    len = length;
08    strcpy(buffer, cstr);
09    return *this;
10}

Что здесь видим? Если reserve вернул не 0, то выполняется самый обыкновенный strcpy - ни о каком освобождении памяти речь не идёт.

Ну, и наконец, посмотрим что там за reserve

1unsigned char String::reserve(unsigned int size)
2{
3    if (buffer && capacity >= size) return 1;
4    if (changeBuffer(size)) {
5        if (len == 0) buffer[0] = 0;
6        return 1;
7    }
8    return 0;
9}

Ну, что если какая-никакая память была выделена (buffer не NULL) и размер той самой выделенной памяти больше size (т.е. в нашем случае - нуля) он просто возвращает 1.

Всё. Случай 

1String s = "Seychas my ee ukorotim";
2// Укорачиваем
3s = "";

разобран полностью.

Никакого освобождения памяти при такой операции не происходит. Нет возражений?

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Да правда Ваша. Проверил с контролем размера кучи. Нет освобождения памяти. В случае с ардуино только финт ушами помог:

1String h="bla-bla-bla"
2char *cls=0;
3h=cls; // тут размер кучи стал равен размеру до объявления h

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, вот, видите, а Вы на "знатоков" катили :-))) Дельный ведь был совет - посмотреть и разобраться :))))

А способ это не единственный. По факту, нужно просто добиться вызова метода invalidate любым способом (прямо его не вызовешь, он protected) или деструктора. Последний можно вызвать, например, фигурными скобками, как в моём примере или просто вызвать его "явно". Только с деструктором надо осторожнее. Если "фигурными скобками", то ничего, а если извратиться и вызвать его "явно", то надо помнить, что этой переменной больше нельзя пользоваться, т.к. он там неаккуратно чистит за собой.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Это ясно. Если необходимо очистить стринг с освобождением памяти где то в вызове другой процедуры куда он передается - ваш способ не подходит :( Поэтому только инвалидэйт кстати для верности нужно вот такое:

1void clear_string(String &b){
2  char *cls=0;
3  b=cls;
4  b="";
5}

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Зачем?