Переполнение String?
- Войдите на сайт для отправки комментариев
Здравствуйте!
Делаю маленький вебсервер. Столкнулся с неожиданной для меня проблемой, собирая Html-страницу из десятка переменных для передачи ее из Arduino Nano по UART, при запросе страницы в браузере, ее содержание не соответствовало ожиданиям. Html документ формировался суммированием строк:
1 | String Content; |
2 | Content= "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n" ; |
3 | Content+=String(var1)+ "<br>\r\n" ; |
4 | Content+=String(var2)+ "<br>\r\n" ; |
5 | Content+=String(var3)+ "<br>\r\n" ; |
6 | ... |
7 | Content+=String(var15)+ "<br>\r\n" ; |
На выходе, в зависимости от количества и длины прибавляемых строк, были куски от ожидаемого.
Проблему решил отправляя по UART не всю страницу сразу в переменной "Content", а отправляя данные частями без собирания в общий пакет (в переменную).
Я так понял, что не хватало памяти, и оно писало абы куда, и на выходе получалось абы что.
Остался теперь вопрос: как узнать максимально возможную длину переменной типа String? До каких пор можно удлинять переменную Content? Может есть какие то способы увидеть, что что то пошло не так?
Пока в оперативку помещается - всё ок, дальше - будут глюки. Отсюда вывод: максимальная длина в байтах, которую можно впихнуть в переменную типа String - это кол-во свободной оперативной памяти.
String - это не переменная, это класс. http://arduino.ru/Reference/StringObject
После компиляции пишет:
Скетч использует 25 828 байт (84%) памяти устройства. Всего доступно 30 720 байт.
Глобальные переменные используют 1 351 байт (65%) динамической памяти, оставляя 697 байт для локальных переменных. Максимум: 2 048 байт.
Как контролировать эту свободную память? А если объявлено несколько переменных? Как однозначно узнать или просчитать грядущий косяк?
Кстати, в HTML \r\n совершенно не нужны.
Где у Вас объявлена переменная Content? Внутри функции?
После компиляции пишет:
Скетч использует 25 828 байт (84%) памяти устройства. Всего доступно 30 720 байт.
Глобальные переменные используют 1 351 байт (65%) динамической памяти, оставляя 697 байт для локальных переменных. Максимум: 2 048 байт.
Как контролировать эту свободную память? А если объявлено несколько переменных? Как однозначно узнать или просчитать грядущий косяк?
В 328 меге всего 2кб памяти - не каждая страница вместится в такой размер. :)
Где у Вас объявлена переменная Content? Внутри функции?
Да внутри.
Да там и небольшая Хтмл - 10 строк по 15-20 символов.
Где у Вас объявлена переменная Content? Внутри функции?
Да внутри.
Да там и небольшая Хтмл - 10 строк по 15-20 символов.
Локальная переменная, объявленная внутри процедуры, хранится в стеке. Что делает дунин компилятор со стеком: сколько его выделяется и как он контролируется я достоверно не знаю, знающие люди подскажут.
200 символов влезут даже в короткую строку. Но не забывайте, что Вы в МК на нищем пайке и тут транжирить память, котрой менее 2-х кБ, налево-направо, склеивать строки для красоты в одну не всегда прокатит. Увеличение длинны строки вообще не простая процедура, да и вообще, строки с завершающим нулём чертовки неудобная штука. Думаю, Вам совершенно не критично за четверь или треть секунды строка будет отдана в порт, потому неизменный заголовок объявляйте как строковую константу и она ляжет в флэш, которой побольще, чем ram, переменные отдавайте по одной.
Где у Вас объявлена переменная Content? Внутри функции?
Это пофиг. String хапает память из кучи malloc'ом
Угу. А "свободной" памяти под malloc() И стек компилятор говорит осталось .. 697 байт .. ВСЕГО. Да, ещё как понимаю у вас utf-8 по сколько там "байт на символ"? А ещё конкатенация строк приводит к realloc() тем или иным способом, и столько раз, сколько конкатенаций имеется .. то бишь имеем ещё и фрагментацию кучи .. а дуньковский malloc() точно умеет дефрагментировать кучу? В какой момент времени? .. :)
И вот оно снова .. то самое безграмотное "классовое" копро-программирование, которое вредно для микроконтроллеров. Так сказать "воочию". А ведь 2 кила оперативы более чем достаточно чтобы сварганить и отправить сраницу, описанную автором темы! :)
В целом, С++ сильный и крайне мощный язык и изучать его безусловно надо и на очень высоком уровне .. хотя бы для того, чтобы понять что на практике, для программирования микроконтроллеров он НАФИГ НЕ НУЖЕН. :)
Некоторые это благополучно понимают и без изучения.
Да, но тем более странно, как эти "некоторые" впику такого понимания НАВЯЗЫВАЮТ С++ начинающим ардуинщикам.
"Копро-программирование - в действии" :)
Но не забывайте, что Вы в МК на нищем пайке и тут транжирить память, котрой менее 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 может не поместиться в оставшуюся память...
Может кто ответит: если невозможно контролировать оставшуюся память, то зачем есть возможность увеличения строк, если есть шанс потерять данные? Набирая вин-программки как то об этом не задумывался, а тут ступор...
Может кто ответит: если невозможно контролировать оставшуюся память,
Так какой же выход у ардуинщика любителя в такой ситуации? Как гарантированно безболезненно работать с увеличением строки? Объявлять массив символов или есть еще варианты?
Ардуинщики-любители - не знаю. Я могу лишь сказать, как в подобных ситуациях действуют профессионалы - хапают всю память, что есть (или что руководитель проекта позволит) и пишут свой, заточенный под конкретную задачу менеджер памяти (своё выделение, освобождение и т.п.). За счёт заточенности под конкретную задачу, всегда получается значительно эффективнее, чем универсальное решение.
1
String Content;
2
Content=
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\n"
;
3
Content+=String(var1)+
"<br>\r\n"
;
4
Content+=String(var2)+
"<br>\r\n"
;
5
Content+=String(var3)+
"<br>\r\n"
;
6
...
7
Content+=String(var15)+
"<br>\r\n"
;
В строке 2 Вы выделяете память точно под символьную константу, хотя знаете. что строка будет расти, но не заботитесь об этом!
В строке 3 Вы освободжаете память, выделенную в строке 2 и запрашиваете новую. Но, поскольку новый кусок больше старого, старый остаётся неиспользованным и хотя формально он свободен, память он продолжает занимать. Если бы Вы хотя бы не использовали там "String(var1)" непонятно зачем, то старый кусок памяти скорее всего бы повторно использовался (точно не уверен, надо смотреть текст malloc, но обычно если кусок с краю, то realloc просто удлиняет его). Но, поскольку Вы использовали "String(var1)", Вы подпёрли старый кусок памяти с хвоста и не дали ему возможности расшириться - заставили менеджер памяти выделять новый кусок.
В строке 4 Вы опять проделываете тоже самое, т.е. хватаете новый кусок памяти, оставив старый просто болтаться и занимать ресурсы.
В строке 5 Вы опять заправшиваете новый кусок памяти и опять старый оставляете занимать место (у Вас уже три куска просто занимают место и их память никак не используется!!!)
И так в каждой строке.
Написать более расточительно по памяти просто очень трудно (хотя, специалисты, конечно, имеются).
Может кто ответит: если невозможно контролировать оставшуюся память, то зачем есть возможность увеличения строк, если есть шанс потерять данные? Набирая вин-программки как то об этом не задумывался, а тут ступор...
Вам уже и я отписал, и ЕвгенийП мой пост "разжевал" .. не надо "работать с увеличением строки" .. и ВООБЩЕ с классами работать НЕ надо!
Все строки загоняются во флеш и достаются оттудова по мере необходимости и сразу, то бишь поштучно, отправляются в ответ. Если есть возможность, то надо запихать во флеш уже готовые ответы ЦЕЛИКОМ и оттадавать их оттуда не расходуя ни одного байта оперативы..
andrey14,
ещё к вопросу об аккуратном программировании работы с памятью (я уже писал, что у Вас написано крайне неаккуратно). Для аккуратной работы в первом приближении не нужно ни каких-то особых знаний, ни выдающегося ума. Нужно просто аккуратно "прибирать за собой", т.е. не оставлять в памяти более ненужные переменные. Так ли это сложно?
Вот, давайте проведём эесперимент по загаживанию памяти экземплярами класса String.
Для начала запустите такой скетч:
01
void
setup
(
void
) {
02
Serial
.begin(115200);
03
String s1(
"1 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
04
Serial
.print(s1);
05
String s2(
"2 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
06
Serial
.print(s2);
07
String s3(
"3 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
08
Serial
.print(s3);
09
String s4(
"4 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
10
Serial
.print(s4);
11
String s5(
"5 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
12
Serial
.print(s5);
13
String s6(
"6 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
14
Serial
.print(s6);
15
String s7(
"7 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
16
Serial
.print(s7);
17
String s8(
"8 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
18
Serial
.print(s8);
19
String s9(
"9 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
20
Serial
.print(s9);
21
String s10(
"10 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
22
Serial
.print(s10);
23
}
24
25
void
loop
(
void
) {}
Как видите. он по идее ложен печатать 10 строк. Но печатает на моей Nano всего четыре (если у Вас Мега - напечатает больше, но всё равно немного).
Почему? Да всё просто. Все переменные, которые уже давно отпечатаны и болеее не нужны, по-прежнему торчат в памяти. Зачем?
Давайте изменим этот скетч всего чуть-чуть, а именно определения строк и их печать заключим в фигурные скобки, чтобы память освобождалась по достижении закрывающей скобки. Вот так:
01
void
setup
(
void
) {
02
Serial
.begin(115200);
03
{
04
String s1(
"1 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
05
Serial
.print(s1);
06
}
07
{
08
String s2(
"2 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
09
Serial
.print(s2);
10
}
11
{
12
String s3(
"3 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
13
Serial
.print(s3);
14
}
15
{
16
String s4(
"4 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
17
Serial
.print(s4);
18
}
19
{
20
String s5(
"5 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
21
Serial
.print(s5);
22
}
23
{
24
String s6(
"6 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
25
Serial
.print(s6);
26
}
27
{
28
String s7(
"7 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
29
Serial
.print(s7);
30
}
31
{
32
String s8(
"8 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
33
Serial
.print(s8);
34
}
35
{
36
String s9(
"9 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
37
Serial
.print(s9);
38
}
39
{
40
String s10(
"10 01234567890012345678900123456789001234567890012345678900123456789001234567890012345678900123456789001234567890\n"
);
41
Serial
.print(s10);
42
}
43
}
44
45
void
loop
(
void
) {}
и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!
Я уже понял в чем косяк, поэтому уже и переписал несколько по-другому.
Использовал
так как var1 и остальные var'ы - float.
String(var1)
А на счет профессионалов, думаю они не маются Ардуино, и не будут в конце проекта впихивать в оставшиеся 5 килобайт менеджер памяти, или может я сильно далек от этого... Статью по вашей ссылке переварить кусок времени надо ;)
ЗЫ Других вариантов (полупрофессиональных) нет? ;)
А на счет профессионалов, думаю они не маются Ардуино, и не будут в конце проекта впихивать в оставшиеся 5 килобайт менеджер памяти, или может я сильно далек от этого...
Сильно далеки :)))
ЗЫ Других вариантов (полупрофессиональных) нет? ;)
Всегда есть 100500 спосроов что-то сделать, на надо задачу знать. А Вы там ещё и флоат пользуете - четверть всей памяти сразу коту под то самое место ...
А про освобождение памяти, помотрите мой пример из предыдущего поста. Не ахти что, но приёмчик действенный, как видите.
и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!
Я в легком шоке ;) Сам бы точно до такого не дошел. Как бы вообще о скобках не задумывался...
А Вы там ещё и флоат пользуете - четверть всей памяти сразу коту под то самое место ...
Что выдают библиотеки, то и использую ;)
Раньше в CVAVR занимал свое время. Там все своими руками напишешь, и все более менее прозрачно. А потом открыл для себя ардуино и поддался на простоту - вставил, подключил, заработало - даунгрейд получается ;) А оказывается нифига местами не проще. А почему глючит - непонятно: или библиотека кривая - лезешь правишь, или сам нахомутал, или вот такое чего еще не знаю.
Ну и на этом спасибо. Буду теперь немного по другому смотреть на память.
и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!
взрыв мозга. слов нет.... можете объяснить это? почему такую глобальную роль играет обособление в скобки?
и о чудо! Всё напечаталось! А всего лишь мы вовремя освобождали ненужную память! И всего делов!
взрыв мозга. слов нет.... можете объяснить это? почему такую глобальную роль играет обособление в скобки?
Это область видимости ;) При выходе за которую локальная переменная становится недоступной, следовательно, память, занимаемая переменной, освобождается. Это вкратце, что называется.
можете объяснить это?
Ну, кратко коллега уже объяснил. А подробно, я скоро выложу третью серию своего эссе про использование памяти. Там это будет одним из двух центральных вопросов.
Кстати, в HTML \r\n совершенно не нужны.
Зачем же вы ерунду говорите ? В хидерах без них никак ! В теле - не нужны.
И что скажут знатоки... Происходит ли освобождение памяти при "укорачивании" стринга, скажем присвоением ему пустой строки или тупо установкой нулевого размера ? ;)
А вообще для любителей юзать длинные текстовые строки имеет смысл посмотреть макросы F() и PSTR() и соответствующие функции, скажем strcpy_p() и уйму таких же других с _p на конце.
Кстати, в HTML \r\n совершенно не нужны.
Зачем же вы ерунду говорите ? В хидерах без них никак ! В теле - не нужны.
Не путайте http и html. :)
А... понятно, а вы типа так никого не запутали ?
А... понятно, а вы типа так никого не запутали ?
Я написал совершенно локанично и точно: в html не нужны. В чём вопрос? Зачем Вы приплели сюда заголовок http запроса? Котлетки отдельно! :)
И что скажут знатоки...
Посоветуют заглянуть в файл
<корень IDE>\hardware\arduino\avr\cores\arduino\WString.cpp
Там всё написано.
Я написал совершенно локанично и точно: в html не нужны. В чём вопрос? Зачем Вы приплели сюда заголовок http запроса? Котлетки отдельно! :)
Может не запроса, а ответа ? Ну если уж мух от котлет отделять.
Там всё написано.
Мда... знатоки оказались не знатоками :) Да мне и ответ был не нужен. Естественно память освобождается, по крайней мере в той версии которую использую я.
Естественно память освобождается,
Совершенно неестественно. Как Вы выразились "при "укорачивании" стринга", скажем присвоением ему пустой строки или тупо установкой нулевого размера" обычно ничего не освобождается.
по крайней мере в той версии которую использую я.
Что ж за версию Вы такую используете? Сами написали или с IDE такая пришла? Если с IDE, то что за версия? В тех IDE, что я видел, сделано классически (так же как в MFC и во многих других реализациях) а именно - с памятью в таких случаях никаких манипуляций вообще не производится.
Теперь настала моя очередь отправить вас в <корень IDE>\hardware\arduino\avr\cores\arduino\WString.cpp :)
Идите и смотрите :)
Идите и смотрите :)
Именно это я и собирался сделать, когда спросил у Вас какая у Вас версия IDE, чтобы мы смотрели одно и тоже. Секрет?
А я посмотрел уже достаточно версий , везде одно и то-же. При присвоении пустой строки стрингу память полностью освобождается :) Даже у ESPшки, не говоря уже о настоящих ардуинах :)
Но именно пустой, а если не пустому стрингу присваивать стоку длинна которой меньше имеющейся, то в этом случае освобождения памяти не происходит.
Ну давайте глянем что у вас там написано. У меня вот:
1
String & 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
}
Чёт я не понял, что значит "пустому стрингу"?
Вы писали: "Происходит ли освобождение памяти при "укорачивании" стринга, скажем присвоением ему пустой строки". Так всё-таки "при укорачивании" или при "присваваивании пустой строке пустую же"?
Вот такая конструкция:
1
String s =
"Seychas my ee ukorotim"
;
2
// Укорачиваем
3
s =
""
;
Это то случай или не тот? Освобождается при таком присваивании память или нет?
Кстати, если это "не тот случай", приведите, пожалуйста, пример присваивания более короткой строки более длинной (укорачивания), которое приведёт к освобождению ранее занимаемой памяти.
Тот случай.
И вот в этом случае, по Вашему, освобождается память?
Ну, что ж, давайте посмотрим. Начнём с самого оператора
1
String & String::
operator
= (
const
char
*cstr)
2
{
3
if
(cstr) copy(cstr, strlen(cstr));
4
else
invalidate();
5
return
*
this
;
6
}
Итак, если переданный параметр не NULL, то он просто вызывает copy и после этого возвращает себя.
Хорошо, смотрим, что там за copy. Вот он родимый:
01
String & 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
1
unsigned
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.
Всё. Случай
1
String s =
"Seychas my ee ukorotim"
;
2
// Укорачиваем
3
s =
""
;
разобран полностью.
Никакого освобождения памяти при такой операции не происходит. Нет возражений?
Да правда Ваша. Проверил с контролем размера кучи. Нет освобождения памяти. В случае с ардуино только финт ушами помог:
1
String h=
"bla-bla-bla"
;
2
char
*cls=0;
3
h=cls;
// тут размер кучи стал равен размеру до объявления h
Ну, вот, видите, а Вы на "знатоков" катили :-))) Дельный ведь был совет - посмотреть и разобраться :))))
А способ это не единственный. По факту, нужно просто добиться вызова метода invalidate любым способом (прямо его не вызовешь, он protected) или деструктора. Последний можно вызвать, например, фигурными скобками, как в моём примере или просто вызвать его "явно". Только с деструктором надо осторожнее. Если "фигурными скобками", то ничего, а если извратиться и вызвать его "явно", то надо помнить, что этой переменной больше нельзя пользоваться, т.к. он там неаккуратно чистит за собой.
Это ясно. Если необходимо очистить стринг с освобождением памяти где то в вызове другой процедуры куда он передается - ваш способ не подходит :( Поэтому только инвалидэйт кстати для верности нужно вот такое:
1
void
clear_string(String &b){
2
char
*cls=0;
3
b=cls;
4
b=
""
;
5
}
Зачем?