1. What is variable scope?
The variable scope determines the visibility and lifetime of a variable. In embedded systems, a correct understanding of scope directly relates to memory usage efficiency and system stability.
Code Example:
// Global variable - visible throughout the program's lifetime
int global_var = 0;
void func() {
// Local variable - visible within the function's stack frame
int local_var = 10;
static int static_var = 0; // Static local variable - lifetime spans the program's execution
static_var++;
global_var++;
}
// File scope variable - visible within the current file
static int file_scope_var = 0;
In resource-constrained embedded systems, it is recommended to:
- Use local variables as much as possible to reduce global memory usage
- Use static variables for data that needs to persist rather than global variables
- Use const to protect critical data from accidental modification
2. What should be done if multiple threads access a variable simultaneously?
This is a typical race condition problem. The core solution is the synchronization mechanism.
Code Example (using POSIX threads):
#include <pthread.h>
int shared_data = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
// Critical section begins
shared_data++; // Safe access
// Critical section ends
pthread_mutex_unlock(&mutex);
return NULL;
}
Design Trade-offs:
- Mutex: Most commonly used, but may cause priority inversion
- Semaphore: Suitable for resource counting scenarios
- Atomic operations: Best performance, but only applicable to simple data types
Think of shared variables as a shared tool room, where the mutex is the only key – whoever has the key can enter and use the tools.Any variable that may be accessed by multiple threads must be protected.
Related Articles: Comparison of Various Communication Methods Between Embedded System Modules
3. How should condition variables be used?
Condition variables are used for efficient waiting and notification between threads, avoiding busy waiting that wastes CPU resources.
Code Example (Producer-Consumer scenario):
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int data_ready = 0;
// Producer thread
void producer() {
pthread_mutex_lock(&mutex);
// Produce data
data_ready = 1;
pthread_cond_signal(&cond); // Notify consumer
pthread_mutex_unlock(&mutex);
}
// Consumer thread
void consumer() {
pthread_mutex_lock(&mutex);
while (!data_ready) { // Must use while to prevent spurious wakeup
pthread_cond_wait(&cond, &mutex);
}
// Consume data
data_ready = 0;
pthread_mutex_unlock(&mutex);
}
Key Points:
- Always use in conjunction with mutex
- Waiting conditions must be checked using a while loop
- Notification can be done with signal (wake one) and broadcast (wake all)
Compared to busy waiting, condition variables can significantly reduce CPU usage.
Related Articles: Summary of Basic Knowledge in Multithreaded Programming!
4. What kind of data structure can implement a producer-consumer model?
A circular buffer/circular queue, message queue.A circular buffer is a good choice because it: efficiently manages data flow in limited memory.
- Avoids dynamic memory allocation
- Provides FIFO semantics
- Cache-friendly

Related Articles: Lightweight Circular Buffer Management Library for Embedded Systems!
Circular Buffer VS Message Queue
A circular buffer (Circular Buffer) and a message queue (Message Queue) are both buffering mechanisms used to pass data between producers and consumers, but they have significant differences in design goals, data processing methods, and applicable scenarios.
A circular buffer (Circular Buffer) and a message queue (Message Queue) are both buffering mechanisms used to pass data between producers and consumers, but they have significant differences in design goals, data processing methods, and applicable scenarios. The following is a detailed analysis from the similarities and differences perspectives:
Similarities
- Support for the “Producer-Consumer” model: Basic logic is the same: producers write data to the buffer, consumers read data from the buffer, and both are decoupled through the buffer (no direct interaction).
- Core functionality is the same: Both serve as an “intermediate layer” for data transmission, solving the problem of mismatched speeds between producers and consumers (e.g., producers generating data faster than consumers can process).
- Depend on synchronization mechanisms: Both need to handle concurrent access issues (e.g., multithreaded read/write), usually relying on mutexes, semaphores, etc., to ensure data consistency (avoiding read/write conflicts).
Differences
| Dimension | Circular Buffer | Message Queue |
|---|---|---|
| Data Structure | Based on a fixed-size array/memory block, implemented by cyclically moving the “read pointer” and “write pointer” | Usually based on linked lists or dynamic arrays, supporting dynamic resizing |
| Data Unit | In units of “byte streams” or “fixed-size data blocks”, with no inherent message boundaries | As “messages” as independent units, each message has clear boundaries (e.g., fixed format, includes length field) |
| Size Flexibility | Fixed size (determined at creation), cannot dynamically resize (if full, must overwrite old data or block) | Can dynamically grow, or configure maximum length (if full, blocks producer or returns error) |
| Priority Support | Does not support priority, strictly follows “first in, first out (FIFO)” (read/write pointer moves in order) | Usually supports message priority (e.g., high-priority messages are consumed first) |
| Overflow Handling | When full, usually has two strategies: 1. Overwrite oldest data (suitable for scenarios with high real-time requirements); 2. Block/return error (to avoid data loss) | When full, usually blocks the producer (waits for consumer to read before writing) or returns an error,will not overwrite existing messages (ensuring message integrity) |
| Memory Efficiency | Memory allocation is fixed, with no dynamic allocation/release overhead, high efficiency | May cause dynamic memory allocation due to variable message sizes, with a risk of memory fragmentation, slightly higher overhead |
| Applicable Scenarios | Suitable for continuous, high-frequency, fixed-size data transmission, such as audio/video stream processing, sensor data collection, internal communication in embedded systems | Suitable for discrete, structured message passing, such as inter-process communication (IPC) |
- Circular Buffer: Advantages include high performance, low overhead, suitable for scenarios with high real-time and memory efficiency requirements (e.g., low-level drivers, streaming media), but low flexibility, requiring manual handling of message boundaries.
- Message Queue: Advantages include high flexibility, support for structured messages and priorities, suitable for complex message passing scenarios (e.g., inter-process/service communication), but slightly higher memory overhead, not suitable for high-frequency continuous data transmission.
5. If you were to design a message queue, what aspects would you consider?
The core of an embedded message queue is memory safety + thread safety + low overhead, the message queue is an extension of the producer-consumer model, requiring support for priority, timeout, and other advanced features, and must adapt to the limited resources of MCUs (small RAM, no MMU).
Several key decision points:
Decision 1: Memory Management – Use static memory pool to avoid fragmentation
- Dynamic malloc in embedded systems can cause memory fragmentation, so pre-allocate a “message node pool” (e.g., 10 nodes, each storing message data).
Decision 2: Thread Safety – Mutex + Semaphore
-
Mutex protects modifications to the queue’s head/tail pointers;
-
Semaphore not_empty: Consumer waits for messages (blocks when the queue is empty);
-
Semaphore not_full: Producer waits for space (blocks when the queue is full).
Decision 3: Message Type – Fixed size, simplifies implementation
- In embedded scenarios, fixed-size messages (e.g., 32B) are more efficient than variable sizes, avoiding dynamic memory calculations.
Decision 4: Priority Support – Insert by priority (optional)
- High-priority messages are inserted at the head of the queue, low-priority at the tail, ensuring urgent messages are processed first.
Decision 5: Timeout Mechanism – Avoid permanent blocking
- If producers/consumers cannot access resources, return an error after a timeout to prevent the system from deadlocking.
// Message node
typedef struct {
uint8_t data[MSG_DATA_SIZE];
uint8_t priority; // 0-low, 3-high
} MsgNode;
// Message queue structure
typedef struct {
MsgNode pool[MSG_QUEUE_SIZE]; // Node pool
uint8_t head; // Read pointer
uint8_t tail; // Write pointer
uint8_t count; // Current number of messages
SemaphoreHandle_t mutex; // Protect queue pointers
SemaphoreHandle_t not_empty; // Consumer semaphore
SemaphoreHandle_t not_full; // Producer semaphore
} MsgQueue;
6. What is the difference between deep copy and shallow copy in constructors?
This relates to object lifecycle management and resource ownership. Shallow copy in constructors may lead to memory-related issues.
Code Comparison:
class String {
private:
char* data;
int length;
public:
// Shallow copy - Dangerous!
String(const String& other) : data(other.data), length(other.length) {
// Just copies the pointer value, both objects point to the same memory
}
// Deep copy - Safe
String(const String& other) : length(other.length) {
data = new char[length + 1];
memcpy(data, other.data, length + 1);
}
~String() {
delete[] data; // This will cause issues!
}
};
void dangerous_example() {
String str1("Hello"); // str1.data points to memory of "Hello"
String str2 = str1; // Shallow copy: str2.data also points to the same memory
}
When the dangerous_example function ends:
-
str2 destructs, delete[] data (frees memory)
-
str1 destructs, delete[] data (frees the same memory again)
-
Result: Double free, program crashes!
Related Articles: Embedded C++ | Introduction to Object-Oriented Programming
Memory Layout of Shallow Copy: Two objects share the same heap memory
Stack Memory:
str1: [data pointer] ──┐
str2: [data pointer] ──┼─→ Heap Memory: ['H']['e']['l']['l']['o']['\0']
Memory Layout of Deep Copy: Each object has its own independent memory copy
Stack Memory:
str1: [data pointer1] ──→ Heap Memory1: ['H']['e']['l']['l']['o']['\0']
str2: [data pointer2] ──→ Heap Memory2: ['H']['e']['l']['l']['o']['\0']
If a class manages dynamic resources, the copy constructor, assignment operator, and destructor must be implemented correctly!
7. What is your understanding of smart pointers?
Smart pointers help us use memory safely in resource-constrained environments.
Smart pointers automatically manage resource lifecycles through the RAII mechanism (Resource Acquisition Is Initialization), fundamentally avoiding memory leaks.
#include <memory>
// unique_ptr - exclusive ownership, zero overhead
std::unique_ptr<Sensor> sensor = std::make_unique<Sensor>();
// shared_ptr - shared ownership, reference counting
std::shared_ptr<DataBuffer> buffer = std::make_shared<DataBuffer>();
// weak_ptr - observer pattern, avoids circular references
std::weak_ptr<Device> device_ref = device_ptr;
| Pointer Type | Usage Scenario | Embedded Considerations |
|---|---|---|
<span>unique_ptr</span> |
Exclusive Resource Sensor, Device Driver | Minimal overhead, prefer to use |
<span>shared_ptr</span> |
Shared Resource Configuration Data, Shared Cache | Be cautious of circular references, avoid overuse |
<span>weak_ptr</span> |
Observe Resource Monitoring, Callback Mechanism | Prevent circular references, check validity |
8. When should virtual functions be used?
Virtual functions are the implementation of “polymorphism”, the core is “parent class pointer calls child class implementation”, used in embedded systems for “unified interface, differentiated implementation”, reducing code redundancy.
Usage Scenario Determination:
class Device {
public:
// Use virtual functions when polymorphism is needed
virtual int read() = 0;
// Use normal functions when polymorphism is not needed
int get_id() const { return id; }
};
class Sensor : public Device {
public:
int read() override { return analog_read(); }
};
Embedded Trade-offs:
- When to use: When a plugin architecture or device abstraction layer is needed
- When to avoid: For real-time tasks that are extremely performance-sensitive
- Alternative: Template strategy pattern (compile-time polymorphism)