汇川嵌入式 软件开发一面 面经

 1.TCP和UDP有什么区别?各自适用什么场景?

TCP和UDP是传输层的两种协议,特点完全不同。

TCP是面向连接的,需要三次握手建立连接,四次挥手断开连接。提供可靠传输,保证数据顺序和完整性,有确认和重传机制。是面向字节流的,数据作为连续的流传输。有流量控制和拥塞控制机制。头部开销大,至少20字节。

UDP是无连接的,直接发送数据,不需要建立连接。不保证可靠性,可能丢包、乱序、重复。是面向数据报的,每个数据包独立传输。没有流量控制和拥塞控制。头部开销小,只有8字节。速度快,实时性好。

// TCP服务器示例
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);  // SOCK_STREAM表示TCP
bind(listen_fd, ...);
listen(listen_fd, 5);
int client_fd = accept(listen_fd, ...);
recv(client_fd, buffer, size, 0);

// UDP服务器示例
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);  // SOCK_DGRAM表示UDP
bind(sock_fd, ...);
recvfrom(sock_fd, buffer, size, 0, ...);


适用场景:

  • TCP适合需要可靠传输的场景:文件传输、网页浏览、邮件、数据库连接
  • UDP适合实时性要求高的场景:视频直播、语音通话、在线游戏、DNS查询

工业控制中,Modbus TCP用TCP保证可靠性,而一些实时控制协议用UDP保证低延迟。

2.TCP为什么会发生粘包?如何解决?

TCP是面向字节流的,没有消息边界的概念。发送方连续发送多个数据包,接收方可能一次性接收到多个包的数据,这就是粘包。

发生粘包的原因:

  • TCP的Nagle算法会将小包合并发送,提高效率
  • 接收方来不及处理,多个包的数据积累在接收缓冲区
  • 发送方发送速度快,接收方处理速度慢

解决方法:

固定长度:每个消息固定长度,不足的用填充字符补齐。简单但浪费空间。

// 固定长度100字节
char buffer[100];
while (true) {
    recv(sock, buffer, 100, 0);  // 每次接收固定100字节
    process_message(buffer);
}


消息头+长度:在消息前加一个固定长度的头部,包含消息长度信息。

struct MessageHeader {
    uint32_t length;  // 消息体长度
};

// 发送
MessageHeader header;
header.length = data.size();
send(sock, &header, sizeof(header), 0);
send(sock, data.c_str(), data.size(), 0);

// 接收
MessageHeader header;
recv(sock, &header, sizeof(header), 0);
char* buffer = new char[header.length];
recv(sock, buffer, header.length, 0);


分隔符:用特殊字符(如\n、\r\n)分隔消息。简单但需要转义特殊字符。

// 发送
std::string msg = "Hello\n";
send(sock, msg.c_str(), msg.size(), 0);

// 接收
std::string buffer;
char c;
while (recv(sock, &c, 1, 0) > 0) {
    if (c == '\n') {
        process_message(buffer);
        buffer.clear();
    } else {
        buffer += c;
    }
}


应用层协议:使用成熟的协议如HTTP、Protobuf等,它们已经解决了粘包问题。

工业控制中,Modbus协议就是用固定格式+CRC校验来解决粘包和数据完整性问题。

3.TCP的MSS是什么?为什么还会分片?

MSS(Maximum Segment Size)是TCP协议中一次传输的最大数据量,在三次握手时协商确定。MSS的目的是避免IP层分片,因为IP分片会降低效率。

MSS的计算:MSS = MTU - IP头部(20字节) - TCP头部(20字节)。以太网MTU通常是1500字节,所以MSS通常是1460字节。

以太网帧:1500字节(MTU)
  - IP头部:20字节
  - TCP头部:20字节
  = TCP数据:1460字节(MSS)


为什么还会分片:

路径MTU不一致:数据包经过的网络路径上,不同链路的MTU可能不同。虽然发送方设置了MSS,但中间路由器的MTU可能更小,仍然需要分片。

IP分片:如果TCP段加上IP头部后超过了路径MTU,IP层会进行分片。虽然TCP协商了MSS,但无法完全避免IP分片。

应用层数据过大:应用层一次发送的数据可能远大于MSS,TCP会将数据分成多个段发送,这不是分片,而是分段。

// 发送大数据
char data[10000];
send(sock, data, 10000, 0);  // TCP会自动分成多个段发送


避免分片的方法:

  • 设置合适的MSS值
  • 使用路径MTU发现(PMTUD)
  • 应用层控制数据大小

工业控制中,通常使用较小的数据包,避免分片带来的延迟和丢包风险。

4.内存泄漏的原因有哪些?如何避免?

内存泄漏是指分配的内存没有被释放,导致可用内存逐渐减少,最终可能导致系统崩溃。

常见原因:

忘记释放内存:new了但没有delete,malloc了但没有free。

void func() {
    int* p = new int(10);
    // 忘记delete p
}  // 内存泄漏


异常导致未释放:函数中途抛出异常,跳过了delete语句。

void func() {
    int* p = new int(10);
    if (error) {
        throw std::exception();  // 跳过delete
    }
    delete p;
}


循环引用:shared_ptr的循环引用导致引用计数永远不为0。

class Node {
public:
    std::shared_ptr<Node> next;
};

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1;  // 循环引用,内存泄漏


容器中的指针:容器存储裸指针,容器销毁时指针指向的对象没有释放。

std::vector<int*> vec;
vec.push_back(new int(10));
// vec销毁时,只释放vector本身,不释放指针指向的内存


避免方法:

使用智能指针:unique_ptr、shared_ptr自动管理内存,不需要手动delete。

void func() {
    std::unique_ptr<int> p = std::make_unique<int>(10);
    // 函数返回时自动释放
}


使用RAII:资源获取即初始化,用对象生命周期管理资源。

class FileHandler {
    FILE* file;
public:
    FileHandler(const char* name) { file = fopen(name, "r"); }
    ~FileHandler() { if (file) fclose(file); }
};


使用容器管理对象:用vector而不是vector<T*>,容器自动管理对象生命周期。

std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10));
// vec销毁时,自动释放所有unique_ptr


使用内存检测工具:Valgrind、AddressSanitizer可以检测内存泄漏。

valgrind --leak-check=full ./myapp


代码审查:重点检查new/delete、malloc/free是否配对。

嵌入式开发中,内存泄漏特别危险,因为内存有限,长时间运行后可能导致系统崩溃。要特别注意内存管理。

5.Go语言和C/C++有什么区别?

Go和C/C++都是系统级编程语言,但设计理念和特性不同。

内存管理:Go有垃圾回收(GC),自动管理内存,不需要手动释放。C/C++需要手动管理内存,容易出现内存泄漏。Go的GC简化了开发,但有性能开销和停顿时间。

并发模型:Go原生支持并发,goroutine是轻量级线程,channel用于通信。C/C++需要使用pthread或std::thread,并发编程更复杂。

// Go的并发
go func() {
    // 并发执行
}()

ch := make(chan int)
ch <- 10  // 发送
x := <-ch  // 接收


// C++的并发
std::thread t([]() {
    // 并发执行
});
t.join();


语法简洁:Go语法简单,没有类、继承、泛型(Go 1.18后支持),学习曲线平缓。C++语法复杂,有很多高级特性。

编译速度:Go编译速度很快,适合快速迭代。C++编译速度慢,大型项目编译可能需要很长时间。

性能:C/C++性能更好,没有GC开销,更接近硬件。Go性能略低,但对大多数应用足够。

生态:C/C++生态成熟,有大量库和工具。Go生态较新,但发展很快,特别是在云计算、微服务领域。

适用场景:

  • C/C++:系统编程、嵌入式、游戏、高性能计算
  • Go:Web服务、微服务、云计算、网络编程

嵌入式开发通常用C/C++,因为资源受限,需要精确控制内存和性能。Go的GC和运行时开销对嵌入式不太友好。

6.list、map、set的区别是什么?

这三个是STL中常用的容器,底层实现和特点不同。

list是双向链表,元素在内存中不连续。插入删除效率高O(1),但随机访问效率低O(n)。不支持下标访问,只能通过迭代器遍历。

std::list<int> lst;
lst.push_back(1);
lst.push_front(2);
lst.insert(lst.begin(), 3);  // O(1)插入
// lst.at(0);  // 错误,不支持下标访问


map是红黑树实现的关联容器,存储键值对,按键自动排序。查找、插入、删除都是O(log n)。键唯一,不能重复。

std::map<std::string, int> m;
m["apple"] = 1;
m["banana"] = 2;
m.find("apple");  // O(log n)查找
// 遍历时按键排序
for (auto& p : m) {
    cout << p.first << ": " << p.second << endl;
}


set也是红黑树实现,存储唯一的元素,自动排序。查找、插入、删除都是O(log n)。元素唯一,不能重复。

std::set<int> s;
s.insert(3);
s.insert(1);
s.insert(2);
s.find(2);  // O(log n)查找
// 遍历时自动排序:1, 2, 3


对比:

list

双向链表

无序

O(n)

O(1)

O(1)

不支持

map

红黑树

有序

O(log n)

O(log n)

O(log n)

不支持

set

红黑树

有序

O(log n)

O(log n)

O(log n)

不支持

还有unordered_map和unordered_set,基于哈希表实现,查找O(1),但无序。

选择建议:

  • 需要频繁插入删除,用list
  • 需要键值对映射,用map或unordered_map
  • 需要去重和排序,用set
  • 追求查找速度,用unordered_map或unordered_set

7.你用过哪些工业通信协议?说说它们的结构

我了解和使用过几种工业通信协议。

Modbus是最常用的工业协议,简单可靠。有Modbus RTU(串口)和Modbus TCP(以太网)两种。

Modbus RTU帧结构:

[从站地址(1字节)] [功能码(1字节)] [数据(N字节)] [CRC校验(2字节)]


功能码:

  • 0x03:读保持寄存器
  • 0x06:写单个寄存器
  • 0x10:写多个寄存器
// Modbus RTU读取示例
uint8_t frame[] = {
    0x01,        // 从站地址
    0x03,        // 功能码:读保持寄存器
    0x00, 0x00,  // 起始地址
    0x00, 0x0A,  // 寄存器数量
    0x00, 0x00   // CRC校验(需要计算)
};


Modbus TCP在Modbus RTU基础上加了MBAP头部:

[事务ID(2字节)] [协议ID(2字节)] [长度(2字节)] [单元ID(1字节)] [功能码+数据]


RS232/RS485是物理层协议,常用于PLC通信。RS232是点对点通信,RS485支持多点通信。

RS232参数:

  • 波特率:常用9600、19200、115200
  • 数据位:8位
  • 停止位:1位
  • 校验位:无校验或偶校验
// 串口配置
struct termios options;
cfsetispeed(&options, B9600);  // 波特率9600
options.c_cflag |= CS8;  // 8数据位
options.c_cflag &= ~PARENB;  // 无校验
options.c_cflag &= ~CSTOPB;  // 1停止位


CAN总线用于汽车和工业控制,支持多主通信,实时性好。

CAN帧结构:

[帧起始] [仲裁段(ID)] [控制段] [数据段(0-8字节)] [CRC] [ACK] [帧结束]


Profibus是西门子的现场总线,速度快,实时性好。

EtherCAT是基于以太网的实时协议,速度很快(微秒级),适合运动控制。

工业协议的特点:

  • 可靠性高:有校验机制(CRC、奇偶校验)
  • 实时性好:确定性的通信延迟
  • 抗干扰强:差分信号、屏蔽电缆
  • 简单实用:协议简单,易于实现

8.你接触过哪些设计模式?详细说说其中一个

我接触过几种常用的设计模式,详细说说单例模式。

单例模式保证一个类只有一个实例,并提供全局访问点。适合全局唯一的对象,如配置管理器、日志系统、数据库连接池等。

实现方式:

懒汉式(延迟初始化):第一次使用时才创建实例。C++11后用局部静态变量实现最简单,线程安全。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证线程安全
        return instance;
    }
    
    void doSomething() {
        // 业务逻辑
    }
    
private:
    Singleton() {}  // 私有构造函数
    ~Singleton() {}
    Singleton(const Singleton&) = delete;  // 禁止拷贝
    Singleton& operator=(const Singleton&) = delete;  // 禁止赋值
};

// 使用
Singleton::getInstance().doSomething();


饿汉式(立即初始化):程序启动时就创建实例。线程安全,但可能浪费资源。

class Singleton {
public:
    static Singleton& getInstance() {
        return instance;
    }
    
private:
    Singleton() {}
    static Singleton instance;  // 静态成员
};

Singleton Singleton::instance;  // 程序启动时初始化


双重检查锁定(不推荐):C++11前的线程安全实现,复杂且容易出错。

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
 

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式面试八股文全集 文章被收录于专栏

这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。

全部评论

相关推荐

评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务