Việc sử dụng malloc()
và free()
dường như khá hiếm trong thế giới Arduino. Nó được sử dụng trong AVR C thuần túy thường xuyên hơn nhiều, nhưng vẫn thận trọng.
Nó có phải là một ý tưởng thực sự tồi để sử dụng malloc()
và free()
với Arduino?
Việc sử dụng malloc()
và free()
dường như khá hiếm trong thế giới Arduino. Nó được sử dụng trong AVR C thuần túy thường xuyên hơn nhiều, nhưng vẫn thận trọng.
Nó có phải là một ý tưởng thực sự tồi để sử dụng malloc()
và free()
với Arduino?
Câu trả lời:
Quy tắc chung của tôi cho các hệ thống nhúng là chỉ malloc()
các bộ đệm lớn và chỉ một lần, khi bắt đầu chương trình, ví dụ: trong setup()
. Rắc rối xảy ra khi bạn phân bổ và phân bổ bộ nhớ. Trong một phiên chạy dài, bộ nhớ bị phân mảnh và cuối cùng việc phân bổ không thành công do thiếu một vùng trống đủ lớn, mặc dù tổng bộ nhớ trống là quá đủ cho yêu cầu.
(Phối cảnh lịch sử, bỏ qua nếu không quan tâm): Tùy thuộc vào việc triển khai trình tải, lợi thế duy nhất của phân bổ thời gian chạy so với phân bổ thời gian biên dịch (toàn cầu hóa) là kích thước của tệp hex. Khi các hệ thống nhúng được xây dựng với các máy tính có kệ có bộ nhớ dễ bay hơi, chương trình thường được tải lên hệ thống nhúng từ mạng hoặc máy tính thiết bị và thời gian tải lên đôi khi là một vấn đề. Loại bỏ bộ đệm đầy số không từ hình ảnh có thể rút ngắn thời gian đáng kể.)
Nếu tôi cần cấp phát bộ nhớ động trong một hệ thống nhúng, tôi thường malloc()
, hoặc tốt nhất là phân bổ tĩnh, một nhóm lớn và chia nó thành các bộ đệm có kích thước cố định (hoặc một nhóm mỗi bộ đệm nhỏ và lớn) và thực hiện phân bổ riêng của tôi / phân bổ lại từ hồ bơi đó. Sau đó, mọi yêu cầu cho bất kỳ dung lượng bộ nhớ nào cho đến kích thước bộ đệm cố định đều được thực hiện với một trong những bộ đệm đó. Hàm gọi không cần biết liệu nó có lớn hơn yêu cầu hay không và bằng cách tránh chia tách và kết hợp lại các khối chúng ta giải quyết phân mảnh. Tất nhiên rò rỉ bộ nhớ vẫn có thể xảy ra nếu chương trình có lỗi cấp phát / giảm cấp phát.
Thông thường, khi viết phác thảo Arduino, bạn sẽ tránh phân bổ động (có thể bằng malloc
hoặc new
cho các phiên bản C ++), mọi người thay vì sử dụng biến toàn cục -or static
- hoặc biến cục bộ (stack).
Sử dụng phân bổ động có thể dẫn đến một số vấn đề:
malloc
/ free
cuộc gọi) trong đó heap phát triển lớn hơn số lượng bộ nhớ thực tế được phân bổ hiện tạiTrong hầu hết các tình huống tôi đã gặp phải, phân bổ động là không cần thiết hoặc có thể tránh được với các macro như trong mẫu mã sau:
MySketch.ino
#define BUFFER_SIZE 32
#include "Dummy.h"
Đồ ngốc
class Dummy
{
byte buffer[BUFFER_SIZE];
...
};
Nếu không #define BUFFER_SIZE
, nếu chúng ta muốn Dummy
lớp có buffer
kích thước không cố định , chúng ta sẽ phải sử dụng phân bổ động như sau:
class Dummy
{
const byte* buffer;
public:
Dummy(int size):buffer(new byte[size])
{
}
~Dummy()
{
delete [] bufer;
}
};
Trong trường hợp này, chúng tôi có nhiều tùy chọn hơn trong mẫu đầu tiên (ví dụ: sử dụng các Dummy
đối tượng khác nhau với buffer
kích thước khác nhau cho từng đối tượng ), nhưng chúng tôi có thể có các vấn đề phân mảnh heap.
Lưu ý việc sử dụng một hàm hủy để đảm bảo bộ nhớ được cấp phát động buffer
sẽ được giải phóng khi một Dummy
thể hiện bị xóa.
Tôi đã xem xét thuật toán được sử dụng bởi malloc()
, từ avr-libc, và dường như có một vài mô hình sử dụng an toàn theo quan điểm phân mảnh heap:
Ý tôi là: phân bổ tất cả những gì bạn cần khi bắt đầu chương trình, và không bao giờ giải phóng nó. Tất nhiên, trong trường hợp này, bạn cũng có thể sử dụng bộ đệm tĩnh ...
Ý nghĩa: bạn giải phóng bộ đệm trước khi phân bổ bất cứ thứ gì khác. Một ví dụ hợp lý có thể trông như thế này:
void foo()
{
size_t size = figure_out_needs();
char * buffer = malloc(size);
if (!buffer) fail();
do_whatever_with(buffer);
free(buffer);
}
Nếu không có malloc bên trong do_whatever_with()
, hoặc nếu chức năng đó giải phóng bất cứ thứ gì nó phân bổ, thì bạn an toàn khỏi bị phân mảnh.
Đây là một khái quát của hai trường hợp trước. Nếu bạn sử dụng heap như một stack (cuối cùng là trước hết), thì nó sẽ hoạt động như một stack và không phải là một đoạn. Cần lưu ý rằng trong trường hợp này, an toàn để thay đổi kích thước bộ đệm được phân bổ cuối cùng với realloc()
.
Điều này sẽ không ngăn chặn sự phân mảnh, nhưng nó an toàn theo nghĩa là đống sẽ không phát triển lớn hơn kích thước tối đa được sử dụng . Nếu tất cả các bộ đệm của bạn có cùng kích thước, bạn có thể chắc chắn rằng, bất cứ khi nào bạn giải phóng một trong số chúng, vị trí sẽ có sẵn cho các lần phân bổ tiếp theo.
Sử dụng phân bổ động (thông qua malloc
/ free
hoặc new
/ delete
) vốn không tệ như vậy. Trong thực tế, đối với một cái gì đó như xử lý chuỗi (ví dụ thông qua String
đối tượng), nó thường khá hữu ích. Đó là bởi vì nhiều bản phác thảo sử dụng một vài chuỗi nhỏ, cuối cùng được kết hợp thành một chuỗi lớn hơn. Sử dụng phân bổ động cho phép bạn chỉ sử dụng nhiều bộ nhớ mà bạn cần cho mỗi bộ nhớ. Ngược lại, sử dụng bộ đệm tĩnh có kích thước cố định cho mỗi bộ đệm có thể sẽ lãng phí rất nhiều dung lượng (khiến nó hết bộ nhớ nhanh hơn nhiều), mặc dù nó phụ thuộc hoàn toàn vào ngữ cảnh.
Với tất cả những gì đã nói, điều rất quan trọng là đảm bảo việc sử dụng bộ nhớ có thể dự đoán được. Cho phép bản phác thảo sử dụng số lượng bộ nhớ tùy ý tùy thuộc vào hoàn cảnh thời gian chạy (ví dụ: đầu vào) có thể dễ dàng gây ra sự cố sớm hay muộn. Trong một số trường hợp, nó có thể hoàn toàn an toàn, ví dụ: nếu bạn biết việc sử dụng sẽ không bao giờ tăng thêm nhiều. Phác thảo có thể thay đổi trong quá trình lập trình mặc dù. Một giả định được đưa ra sớm có thể bị lãng quên khi một cái gì đó được thay đổi sau đó, dẫn đến một vấn đề không lường trước được.
Để mạnh mẽ, thường tốt hơn khi làm việc với các bộ đệm có kích thước cố định nếu có thể và thiết kế bản phác thảo để hoạt động rõ ràng với các giới hạn đó ngay từ đầu. Điều đó có nghĩa là bất kỳ thay đổi nào trong tương lai đối với bản phác thảo, hoặc bất kỳ tình huống thời gian chạy bất ngờ nào, hy vọng sẽ không gây ra bất kỳ vấn đề nào về bộ nhớ.
Tôi không đồng ý với những người nghĩ rằng bạn không nên sử dụng nó hoặc nó thường không cần thiết. Tôi tin rằng nó có thể nguy hiểm nếu bạn không biết về nó, nhưng nó rất hữu ích. Tôi có những trường hợp mà tôi không biết (và không quan tâm để biết) kích thước của cấu trúc hoặc bộ đệm (tại thời gian biên dịch hoặc thời gian chạy), đặc biệt là khi nói đến các thư viện tôi gửi ra thế giới. Tôi đồng ý rằng nếu ứng dụng của bạn chỉ xử lý một cấu trúc duy nhất, đã biết, bạn chỉ nên nướng ở kích thước đó vào thời gian biên dịch.
Ví dụ: Tôi có một lớp gói nối tiếp (một thư viện) có thể lấy các tải trọng dữ liệu có độ dài tùy ý (có thể là struct, mảng của uint16_t, v.v.). Khi kết thúc việc gửi lớp đó, bạn chỉ cần nói cho phương thức Packet.send () địa chỉ của thứ bạn muốn gửi và cổng Phần cứng thông qua đó bạn muốn gửi nó. Tuy nhiên, ở đầu nhận, tôi cần một bộ đệm nhận được phân bổ động để giữ tải trọng đến đó, vì tải trọng đó có thể là một cấu trúc khác nhau tại bất kỳ thời điểm nào, tùy thuộc vào trạng thái của ứng dụng. NẾU tôi chỉ gửi một cấu trúc duy nhất qua lại, tôi chỉ làm cho bộ đệm có kích thước cần thiết trong thời gian biên dịch. Nhưng, trong trường hợp các gói có thể có độ dài khác nhau theo thời gian, malloc () và free () không quá tệ.
Tôi đã chạy thử nghiệm với mã sau trong nhiều ngày, để nó lặp lại liên tục và tôi không tìm thấy bằng chứng nào về sự phân mảnh bộ nhớ. Sau khi giải phóng bộ nhớ được cấp phát động, số tiền miễn phí sẽ trở về giá trị trước đó.
// found at learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
uint8_t *_tester;
while(1) {
uint8_t len = random(1, 1000);
Serial.println("-------------------------------------");
Serial.println("len is " + String(len, DEC));
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("alloating _tester memory");
_tester = (uint8_t *)malloc(len);
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
Serial.println("Filling _tester");
for (uint8_t i = 0; i < len; i++) {
_tester[i] = 255;
}
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("freeing _tester memory");
free(_tester); _tester = NULL;
Serial.println("RAM: " + String(freeRam(), DEC));
Serial.println("_tester = " + String((uint16_t)_tester, DEC));
delay(1000); // quick look
}
Tôi chưa thấy bất kỳ sự suy giảm nào trong RAM hoặc trong khả năng phân bổ nó một cách linh hoạt bằng phương pháp này, vì vậy tôi nói đó là một công cụ hữu hiệu. FWIW.
Có phải là một ý tưởng thực sự tồi khi sử dụng malloc () và free () với Arduino?
Câu trả lời ngắn gọn là có. Dưới đây là những lý do tại sao:
Đó là tất cả về việc hiểu MPU là gì và cách lập trình trong các ràng buộc của các tài nguyên có sẵn. Arduino Uno sử dụng MPU ATmega328p với bộ nhớ flash ISP 32KB, EEPROM 1024B và SRAM 2KB. Đó không phải là nhiều tài nguyên bộ nhớ.
Hãy nhớ rằng SRK 2KB được sử dụng cho tất cả các biến toàn cục, chuỗi ký tự, ngăn xếp và khả năng sử dụng của heap. Ngăn xếp cũng cần phải có phòng đầu cho ISR.
Các bố trí bộ nhớ là:
Ngày nay PC / laptop có dung lượng bộ nhớ gấp hơn 1.000.000 lần. Không gian ngăn xếp mặc định 1 Mbyte cho mỗi luồng không phải là hiếm nhưng hoàn toàn không thực tế trên MPU.
Một dự án phần mềm nhúng phải làm một ngân sách tài nguyên. Đây là ước tính độ trễ ISR, không gian bộ nhớ cần thiết, sức mạnh tính toán, chu kỳ hướng dẫn, v.v ... Thật không may, không có bữa ăn trưa miễn phí và lập trình nhúng thời gian thực khó là kỹ năng lập trình khó nhất để thành thạo.
Ok, tôi biết đây là một câu hỏi cũ nhưng tôi càng đọc qua các câu trả lời, tôi càng tiếp tục quay lại với một quan sát có vẻ nổi bật.
Dường như có một mối liên hệ với vấn đề dừng của Turing ở đây. Cho phép phân bổ động làm tăng tỷ lệ nói 'tạm dừng' để câu hỏi trở thành một trong những chấp nhận rủi ro. Mặc dù thuận tiện để loại bỏ khả năng malloc()
thất bại và vv, nó vẫn là một kết quả hợp lệ. Câu hỏi mà OP yêu cầu chỉ xuất hiện là về kỹ thuật, và vâng, các chi tiết của các thư viện được sử dụng hoặc MPU cụ thể có vấn đề; cuộc trò chuyện hướng tới việc giảm nguy cơ tạm dừng chương trình hoặc bất kỳ kết thúc bất thường nào khác. Chúng ta cần nhận ra sự tồn tại của môi trường chấp nhận rủi ro rất khác nhau. Dự án sở thích của tôi là hiển thị màu sắc đẹp trên dải đèn LED sẽ không giết ai đó nếu có điều gì đó bất thường xảy ra nhưng MCU bên trong một cỗ máy tim phổi có thể sẽ xảy ra.
Đối với dải đèn LED của tôi, tôi không quan tâm nếu nó bị khóa, tôi sẽ chỉ đặt lại nó. Nếu tôi sử dụng máy trợ tim do MCU điều khiển, hậu quả của việc khóa hoặc không hoạt động theo nghĩa đen là sự sống và cái chết, vì vậy câu hỏi về malloc()
và free()
nên được phân chia giữa cách chương trình dự định đối phó với khả năng chứng minh Mr. Vấn đề nổi tiếng của Turing. Có thể dễ dàng quên rằng đó là một bằng chứng toán học và để thuyết phục bản thân rằng nếu chỉ cần chúng ta đủ thông minh, chúng ta có thể tránh được sự bất thường về giới hạn tính toán.
Câu hỏi này nên có hai câu trả lời được chấp nhận, một cho những người bị buộc phải chớp mắt khi nhìn chằm chằm vào Vấn đề dừng lại ở mặt và một cho tất cả những người khác. Mặc dù hầu hết việc sử dụng arduino có thể không thực hiện các ứng dụng quan trọng hoặc sinh tử, sự khác biệt vẫn còn đó bất kể bạn có thể mã hóa MPU nào.
Không, nhưng chúng phải được sử dụng rất cẩn thận đối với bộ nhớ được cấp phát miễn phí (). Tôi chưa bao giờ hiểu tại sao mọi người nói nên tránh quản lý bộ nhớ trực tiếp vì nó hàm ý mức độ không đủ năng lực thường không tương thích với phát triển phần mềm.
Hãy nói rằng bạn sử dụng arduino của bạn để điều khiển máy bay không người lái. Bất kỳ lỗi nào trong bất kỳ phần nào trong mã của bạn đều có khả năng khiến nó rơi khỏi bầu trời và làm tổn thương ai đó hoặc điều gì đó. Nói cách khác, nếu ai đó thiếu khả năng sử dụng malloc, họ có thể không nên mã hóa chút nào vì có rất nhiều lĩnh vực khác mà các lỗi nhỏ có thể gây ra vấn đề nghiêm trọng.
Là các lỗi gây ra bởi malloc khó theo dõi và sửa chữa? Vâng, nhưng đó là vấn đề thất vọng nhiều hơn về phần mã hóa hơn là rủi ro. Theo như rủi ro, bất kỳ phần nào trong mã của bạn đều có thể rủi ro như nhau hoặc nhiều hơn so với malloc nếu bạn không thực hiện các bước để đảm bảo rằng nó được thực hiện đúng.