影石嵌入式开发二面准备(2w字长文全面复盘)

目录

🌼前言

另一家面试的不足

真正的前言

后记

🌹一,TinyWebServer && C语言

1,和蓝牙 WiFi 的联系

2,基础概念

3,项目架构(分模块)

① 架构图 

② 核心模块(9 大模块)

③ 完整流程

4,GDB详细调试过程

第一步:发现服务器卡死

第二步:使用GDB附加进程

第三步:分析所有线程状态

第四步:查看每个线程的详细堆栈

第五步:检查关键变量状态

第六步:分析死锁形成过程

😔二,C 语言基础

1. 指针与数组的本质区别

基本概念对比

关键区别展示

函数传参的陷阱

2. 动态内存管理的完整机制

三大内存分配函数对比

内存管理的最佳实践

3. 函数指针的深度应用

概念

回调机制

状态机实现

C 语言模拟多态

4. 结构体内存对齐

对齐规则详解

实际对齐示例

对齐的影响和控制

5. volatile 关键字

volatile 的深层含义

三大应用场景

底层机制

总结

6. 位运算在底层控制中的精妙应用

四大基础操作

高级位操作技巧

嵌入式开发实战

7. 联合体的内存共享巧用

内存共享机制

经典应用模式

实际应用价值

8. 变量作用域和生命周期的精确控制

三种静态变量对比

内存分布差异

9. 字符串操作的安全防护

危险函数的安全替代

长度计算的陷阱

缓冲区溢出防护

10. 预处理器的高级应用技巧

宏定义的安全写法

条件编译的实战应用

高级预处理技巧

#pragma


🌼前言

另一家面试的不足

补充:2025/6/12,面了另一家测开,某大中厂游戏测试开发(偏向开发,各种测试工具),一面二面,4 个不足

算法太久没刷了,easy 和简单的 medium 都做的磕磕绊绊

② 必须要针对当前开发的项目,结合当前各大公司,常用的压测工具,进行全方位的针对性测试,并由测试结果推导,去修改代码逻辑,产生性能提升,稳定性提升等等

③ 测试用例的场景题,没有准备,比如《王者荣耀》登录的测试用例,能不能快速说出三四十条,比如猎魂觉醒,新出的武器,能不能在 5 分钟内讲出 40 条可行的测试点

④ 遇到自己不熟悉,不会的部分,不要说自己最近没用过,没玩过,要说类似的,自己会的,去引导面试官,比如说,“我知道另一个类似的 xxx 机制 / 框架 / 技术 / 逻辑,可以讲这个吗”

真正的前言

  • 内心 os:最近一周还通宵 3 天打游戏,对,就是那个《英勇之地》,steam 同款手游
  • 突然来了面试,一面还过了,那就好好准备吧

影石一面过了,纯属运气,投的测试岗,分到了开发(拿 offer,运气占 50%

刚好之前接触了一点 gdb 调试,vs 调试,以及C语言内存泄露等等

二面业务面,应该会针对项目,C语言基础,更深入,难度也更大

不管了,尽力就好(指的是,写这一篇博客来复盘)

半吊子水平,啥也不会,只能靠写博客安慰下自己的样子

PS:面试官真好,这不得尽力,就算二面失之交臂,2 天时间,留下了这篇博客,也不亏

因为昨天问了两个C语言的知识

比如函数指针,就是函数回调(理解成函数传参的二级指针了)都没答上来

然后项目的模块,昨天自我介绍,没有提到,只是说了 25 分钟实习产出

面试官对你这个项目做了什么,还不是很清楚,加上是一年半前做的项目了

回答时也没回答的很透彻,比如问你在哪一层上做的注册登录,或者状态机

还有蓝牙协议栈之类的,和项目中的 TCP/IP 协议栈的相似点

以及C语言中,怎么定位内存泄露等等

🗡🗡大二,面了 4 次,0 offer🗡🗡

🗡🗡25年2月,面了 6 次,1 offer,一面 20 分钟,项目吹了 15 分钟,直接发 offer 了😂🗡🗡 

🗡🗡25年6月,影石就是第一次面试,要是第 1 次就能有 offer,后面就不面了🗡🗡 

① 其实可以作个量化,同等学历 / 技术,你面第 1 次拿不到 offer,面第 20 次能拿到大厂测试或者小厂开发的 offer,面第 50 次,大厂开发二面三面随便进了....

② 面的越多,复盘越多,你对面试的了解就越多,甚至面试官问了上句,你要能预判了他后面想问的东西(他会对什么东西感兴趣呢,你准备好自己的稿子了吗),进一步引导面试的

节奏

(不同岗位问的东西,其实都差不多;

同一家公司,今年问的,和去年问的,也差不多😂

甚至我对比过某些大厂,5年前,也就是 2020 年的面经,

和今年,2025 年的面经,小部分甚至就是原题,大部分雷同,换汤不换药😂)

a. 面试官关心的是,你的C语言,C++,项目,算法能力,是否适配他们的要求

b. 到了公司,能不能快速上手

c. 或者有没有工作相关的技能,以及你的学习能力和对语言基础 / 业务的思考,能不能替他们解决业务中的痛点,分担压力(所以,从这个方向出发,去准备你的面试)

d. 能不能写出不内存泄露,没有逻辑 bug,代码风格良好,项目上线后不会出问题的代码,甚至协助同事去优化代码

结合 TinyWebServer 专栏食用更佳:TinyWebServer_千帐灯无此声的博客-CSDN博客 

后记

拿 offer 了....真的是运气....同时感谢第一段实习,没有第一段实习打底,肯定面不过的

这几天找房子,然后,另外出一篇博客,复习下 C 语言,保证入职能快速上手项目

第一段实习,中望,是2025年2月,第一次面试,拿的 offer

第二段实习,影石,是2025年6月,第一次面试,拿的 offer

就是,拿 offer 了,不管测开还是开发,都好好干,可能比较缺人吧,加上运气使然

自身的技术其实真的很烂,所以更得多点加班,赶上进度,还有借助 cursor 和 公司文档

没事,多问问题,问过的问题都记录下来,防止后面遇到了,又去打扰别人

而且,不要逮着 mentor 或者某个同事薅羊毛,要雨露均沾,都问问

就像在中望一样,因为自己菜嘛哈哈,实习生 5 点半可以下班,自己前期经常呆到 7 点

那么影石,如果实习生,一般 8 点下班的话,自己呆到 9 点,也不是不行哈哈

🌹一,TinyWebServer && C语言

对于下列基础,或者面试要点,可以结合 cursor 进行进一步了解和背诵,以便面试可以正确回答

1,和蓝牙 WiFi 的联系

尽管 Linux 服务器框架,是网络服务器,但是和蓝牙 WiFi 嵌入式开发有很多相似点

① 网络编程基础

  • Socket 编程 --> 蓝牙/WiFi 都基于 Socket 通信
  • TCP/IP 协议栈 --> 无线通信协议栈
  • 数据包解析 --> 蓝牙/WiFi 帧解析

② 多线程并发

  • 线程池 --> 蓝牙多连接
  • 线程竞争(锁,信号量) --> 硬件资源竞争
  • 异步事件处理 --> 中断处理

③ 底层系统编程

  • 文件描述符 --> 硬件设备句柄
  • epoll 事件驱动 --> 硬件中断驱动
  • 内存管理 --> 嵌入式内存优化

2,基础概念

Ⅰ Socket, epoll, TCP 的联系

🌐 网络通信基础概念关系图
================================================================================
                          【客户端】                    【服务器端】
                    ┌─────────────────┐           ┌─────────────────┐
                    │   Web浏览器      │           │  TinyWebServer  │
                    │                 │           │                 │
                    │  ┌───────────┐  │           │  ┌───────────┐  │
                    │  │  Socket   │  │           │  │  Socket   │  │
                    │  │  (连接)   │  │◄─────────►│  │  (监听)   │  │
                    │  └───────────┘  │           │  └───────────┘  │
                    └─────────────────┘           └─────────────────┘
                             │                             │
                             ▼                             ▼
                    ┌─────────────────┐           ┌─────────────────┐
                    │      TCP        │           │      TCP        │
                    │   (传输层)       │◄─────────►│   (传输层)       │
                    │  三次握手建连    │           │  listen/accept  │
                    └─────────────────┘           └─────────────────┘
                             │                             │
                             ▼                             ▼
                    ┌─────────────────┐           ┌─────────────────┐
                    │      IP         │           │      IP         │
                    │   (网络层)       │◄─────────►│   (网络层)       │
                    └─────────────────┘           └─────────────────┘

🔍 服务器端详细机制:
┌─────────────────────────────────────────────────────────────────┐
│                     TinyWebServer 内部机制                      │
│                                                                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│  │  listenfd   │    │   epollfd   │    │  clientfd   │         │
│  │ (监听socket) │    │ (事件管理器)│    │ (客户连接)   │         │
│  │             │    │             │    │             │         │
│  │ 负责监听     │───→│ 统一管理     │◄──│ 数据传输     │         │
│  │ 新连接请求   │    │ 所有事件     │   │ 读写操作     │         │
│  └─────────────┘    └─────────────┘    └─────────────┘         │
│         │                   │                   │              │
│         ▼                   ▼                   ▼              │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                  内核空间                                │   │
│  │                                                         │   │
│  │  socket缓冲区 ◄──► epoll红黑树 ◄──► 网络数据包           │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Ⅱ 核心概念

① listenfd,epollfd,clientfd 区别

listenfd:监听 Socket,用于接收新的客户端连接请求

epollfd:epoll 实例,管理需要监听的文件描述符

clientfd:客户端连接 Socket,与具体客户端通信,recv() / send()

② 为什么 epoll_wait() 先检测,而不是 TCP 先发送

因为服务器是被动响应的,不是主动推动模式

时间线演示:

T1: 服务器启动
    Server: socket() → bind() → listen() → epoll_wait()
    状态:等待连接,进入阻塞状态

T2: 客户端发起连接  
    Client: socket() → connect(server_ip:port)
    网络:发送SYN包到服务器

T3: 服务器检测到连接事件
    Server: epoll_wait() 返回,检测到listenfd有EPOLLIN事件
    原因:内核接收到SYN包,将连接放入监听队列

T4: 服务器接受连接
    Server: accept() → 创建新的clientfd
    网络:三次握手完成,连接建立

T5: 客户端发送HTTP请求
    Client: send("GET /index.html HTTP/1.1\r\n...")
    网络:数据包通过已建立的TCP连接传输

T6: 服务器检测到数据事件
    Server: epoll_wait() 返回,检测到clientfd有EPOLLIN事件
    原因:内核接收缓冲区有数据

关键理解:
❓ 为什么服务器要"等待"?
✅ 因为服务器不知道客户端什么时候会连接或发送数据
✅ epoll_wait()是"被动等待"机制,响应客户端的主动请求
✅ 这就是"服务器"的本质:为客户端提供服务,而不是主动推送

③ epoll 的作用机制

epoll 和 传统方法对比

  • select():O(n) 时间复杂度,fd 数量限制 1024,需要遍历所有 fd
  • poll():O(n),无 fd 数量限制,需要遍历所有 fd
  • epoll():O(1),事件驱动,内核直接返回就绪事件

epoll 核心机制

  1. 将 fd 注册到 epoll 实例
  2. epoll_wait() 阻塞等待,直到有事件就绪
  3. 内核直接返回就绪事件列表,不需要轮询

3,项目架构(分模块)

① 架构图 

🌐 TinyWebServer 完整架构图
================================================================================
                           【网络层次模型】
        ┌─────────────────────────────────────────────────────────────┐
        │                    应用层 (Application Layer)                │
        │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
        │  │  日志系统    │  │  配置管理    │  │  Web页面     │          │
        │  │ (LOG)       │  │ (Config)    │  │ (HTML/CSS)  │          │
        │  └─────────────┘  └─────────────┘  └─────────────┘          │
        └─────────────────────────────────────────────────────────────┘
                                    ║
        ┌─────────────────────────────────────────────────────────────┐
        │               会话层/表示层 (Session/Presentation)            │
        │         ┌─────────────────────────────────────┐              │
        │         │        HTTP 协议解析模块              │              │
        │         │                                     │              │
        │         │  ┌─────────────┐  ┌─────────────┐   │              │
        │         │  │主状态机      │  │从状态机      │   │              │
        │         │  │(解析请求行)  │  │(替换/r/n)  │   │              │
        │         │  └─────────────┘  └─────────────┘   │              │
        │         │           ║              ║          │              │
        │         │  ┌─────────────────────────────┐   │              │
        │         │  │    HTTP_CODE 状态管理        │   │              │
        │         │  └─────────────────────────────┘   │              │
        │         └─────────────────────────────────────┘              │
        └─────────────────────────────────────────────────────────────┘
                                    ║
        ┌─────────────────────────────────────────────────────────────┐
        │                  传输层 (Transport Layer)                   │
        │    ┌─────────────┐              ┌─────────────┐             │
        │    │  线程池      │◄────────────►│ 连接管理     │             │
        │    │(ThreadPool) │              │(Connection)  │             │
        │    │             │              │             │             │
        │    │工作线程队列  │              │ ┌─────────┐  │             │
        │    │任务分发机制  │              │ │定时器    │  │             │
        │    │生产者消费者  │              │ │链表管理  │  │             │
        │    └─────────────┘              │ └─────────┘  │             │
        │                                 └─────────────┘             │
        └─────────────────────────────────────────────────────────────┘
                                    ║
        ┌─────────────────────────────────────────────────────────────┐
        │                   网络层 (Network Layer)                    │
        │              ┌─────────────────────────────┐                │
        │              │        epoll 事件驱动        │                │
        │              │                             │                │
        │              │  ┌─────────┐ ┌─────────┐   │                │
        │              │  │监听事件  │ │I/O事件   │   │                │
        │              │  │EPOLLIN  │ │EPOLLOUT │   │                │
        │              │  └─────────┘ └─────────┘   │                │
        │              └─────────────────────────────┘                │
        └─────────────────────────────────────────────────────────────┘
                                    ║
        ┌─────────────────────────────────────────────────────────────┐
        │                  数据链路层 (Data Link Layer)                │
        │       ┌─────────────┐            ┌─────────────┐            │
        │       │ Socket通信   │            │ MySQL连接池  │            │
        │       │             │            │             │            │
        │       │ TCP Socket  │            │ 数据库连接   │            │
        │       │ 非阻塞I/O   │            │ RAII管理     │            │
        │       │ 地址复用    │            │ 连接复用     │            │
        │       └─────────────┘            └─────────────┘            │
        └─────────────────────────────────────────────────────────────┘

📊 数据流向图:
客户端请求 → Socket接收 → epoll事件触发 → 线程池分发 → HTTP解析(主从状态机) 
    → 业务处理 → 数据库操作 → 响应生成 → Socket发送 → 日志记录

② 核心模块(9 大模块)

epoll 事件驱动核心(网络层)

位置:网络层,负责 I/O 多路复用

任务:监听 / 分发网络事件

实现思路:

eventLoop() 核心逻辑:
while (!stop_server) {
    // 等待事件就绪
    number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    
    // 遍历所有就绪事件
    for (i = 0; i < number; i++) {
        sockfd = events[i].data.fd;
        
        if (sockfd == listenfd) {
            // 新连接事件 → 调用accept()
            dealclinetdata();
        } else if (events[i].events & EPOLLIN) {
            // 读事件 → 分发给线程池
            dealwithread(sockfd);
        } else if (events[i].events & EPOLLOUT) {
            // 写事件 → 发送响应
            dealwithwrite(sockfd);
        }
    }
}

难点:

  • 边缘触发 vs 水平触发:ET 模式需要一次性读完所有数据
  • 事件处理顺序:信号处理优先级最高,然后是连接事件
  • 非阻塞 I/O 配合:必须设置 Socket 为非阻塞模式

HTTP 协议解析(会话层 / 表示层)

位置:会话层,负责 HTTP 协议的解析和处理

核心:主从状态机模式

主从状态机转换:

CHECK_STATE_REQUESTLINE → CHECK_STATE_HEADER → CHECK_STATE_CONTENT

process_read() 核心逻辑:
while (继续解析条件) {
    switch (m_check_state) {
        case CHECK_STATE_REQUESTLINE:
            // 解析 "GET /index.html HTTP/1.1"
            parse_request_line(text);
            break;
        case CHECK_STATE_HEADER:
            // 解析 "Host: localhost:9006"
            parse_headers(text);
            break;
        case CHECK_STATE_CONTENT:
            // 解析POST数据
            parse_content(text);
            break;
    }
}

从状态机职责:

  • 依次读取每一行,将末尾的 \r\n 替换成 \0\0(从缓冲区读取)
  • 提供 LINE_OK,LINE_BAD,LINE_OPEN 三种状态(成功,失败,读取中)
  • 为主状态机提供行级数据(字符串形式交给主状态机)

线程池并发处理(传输层)

位置:传输层,负责并发任务处理

模式:生产者消费者模式

实现思路:

工作线程循环:
while (true) {
    // 等待任务信号
    m_queuestat.wait();
    
    // 加锁取任务
    m_queuelocker.lock();
    request = m_workqueue.front();
    m_workqueue.pop_front();
    m_queuelocker.unlock();
    
    // 处理任务
    if (Reactor模式) {
        if (读任务) {
            request->read_once();
            request->process();
        } else {
            request->write();
        }
    }
}

Reactor vs Proactor:

  • Reactor:线程池负责 I/O 操作 + 任务处理
  • Proactor:主线程负责 I/O,线程池只处理业务逻辑

定时器(传输层)

位置:传输层,管理连接生命周期

数据结构:升序双向链表

核心功能:

  • 连接超时检测:定期清理超时连接
  • 资源回收:自动关闭无效连接
  • 链表维护:按超时时间升序排列,便于批量清理

数据库连接池(数据链路层)

位置:数据链路层,管理数据库资源

设计:单例模式 + 对象池模式

核心机制:

连接获取:
GetConnection() {
    reserve.wait();          // P操作,等待可用连接
    lock.lock();
    con = connList.front();  // 取出连接
    connList.pop_front();
    lock.unlock();
    return con;
}

连接释放:
ReleaseConnection(con) {
    lock.lock();
    connList.push_back(con); // 归还连接
    lock.unlock();
    reserve.post();          // V操作,释放信号
}

RAII 管理:使用 connectionRAII 类,自动管理连接生命周期

日志系统(应用层)

位置:应用层,记录系统运行状态

设计模式:单例模式 + 生产者消费者模式

核心:

  • 分级日志:DEBUG,INFO,WARN,ERROR 四个级别
  • 按天分文件:自动按日期创建新的日志文件
  • 异步写入:使用阻塞队列,实现异步日志写入
    (怎么实现异步的?日志初始化函数 Log::init() 中,有个 max_queue_size > 0 就异步)
  • 线程安全:多线程环境下的安全日志记录
    (怎么保证多线程安全?C++11的懒汉单例模式,局部静态变量,以及私有构造析构)
    (单例:全局唯一日志实例 + 资源共享 / 所有线程共享一个日志文件)

线程池同步(传输层)

位置:传输层,管理工作线程的任务分配和同步

数据结构:生产者-消费者队列 + 信号量机制

核心功能:

  • 任务队列管理:使用 std::list<T *> m_workqueue 存储待处理任务
  • 信号量控制:通过 sem m_queuestat 实现资源计数,控制任务数量
  • 互斥锁保护:locker m_queuelocker 保护任务队列临界区的访问
  • 生产者消费者:主线程添加任务(append),工作线程取出任务(run)

关键代码:

// 任务添加(生产者)
bool append(T *request, int state) {
    m_queuelocker.lock();           // 加锁保护
    m_workqueue.push_back(request); // 添加任务
    m_queuelocker.unlock();         // 解锁
    m_queuestat.post();             // V操作,通知消费者
}

// 任务处理(消费者)
void run() {
    while (true) {
        m_queuestat.wait();         // P操作,等待任务
        m_queuelocker.lock();       // 加锁获取任务
        T *request = m_workqueue.front();
        m_workqueue.pop_front();
        m_queuelocker.unlock();     // 解锁
        request->process();         // 处理任务
    }
}

阻塞队列通信机制(会话层)

位置:会话层,日志系统异步写入

数据结构:循环队列 + 条件变量

核心:

  • 线程间数据传递:阻塞队列,在写线程和日志线程间传递消息
  • 条件变量通信:使用 cond m_cond 实现线程间的,等待唤醒机制
  • 超时处理:支持带超时的 pop 操作,避免无限等待
  • 队列状态管理:自动管理队列 满 / 空 的状态

关键代码:

// 生产者推送数据
bool push(const T &item) {
    m_mutex.lock();
    m_array[m_back] = item;        // 数据写入
    m_size++;
    m_cond.broadcast();            // 广播通知所有等待线程
    m_mutex.unlock();
}

// 消费者获取数据
bool pop(T &item) {
    m_mutex.lock();
    while (m_size <= 0) {          // 队列为空时等待
        m_cond.wait(m_mutex.get()); // 条件变量等待
    }
    item = m_array[m_front];       // 取出数据
    m_size--;
    m_mutex.unlock();
}

信号管道通信(网络层)

位置:网络层,异步信号处理机制

数据结构:Unix 域 Socket 管道对

核心:

  • 信号异步处理:异步信号转换为同步的 I/O 事件
  • 管道通信:通过 socketpair 创建的管道传递信号信息
  • epoll 统一管理:将信号管道加入 epoll,统一事件处理
  • 进程间解耦:信号处理函数,只是写管道,主循环负责处理

关键代码:

// 信号处理器(写端)
void sig_handler(int sig) {
    char a = sig;
    send(u_pipefd[1], &a, 1, 0);  // 将信号值写入管道
}

// 主循环处理(读端)
bool dealwithsignal(bool &timeout, bool &stop_server) {
    char signals[1024];
    int ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
    for (int i = 0; i < ret; ++i) {
        switch (signals[i]) {      // 根据信号值执行不同逻辑
            case SIGALRM: timeout = true; break;
            case SIGTERM: stop_server = true; break;
        }
    }
}

③ 完整流程

📊 完整请求处理流程:

1. 网络层:epoll_wait() 检测到读事件
   ↓
2. 传输层:dealwithread() 将任务加入线程池队列
   ↓  
3. 传输层:工作线程从队列取出任务
   ↓
4. 会话层:调用 http_conn::process() 
   ↓
5. 会话层:主状态机解析HTTP请求
   ├─ 从状态机解析行格式
   ├─ 解析请求行、请求头、请求体
   └─ 生成HTTP_CODE状态
   ↓
6. 应用层:根据请求类型处理业务逻辑
   ├─ 静态文件服务(mmap内存映射)
   ├─ 动态页面处理(登录注册)
   └─ 数据库操作(连接池获取连接)
   ↓
7. 会话层:生成HTTP响应
   ↓
8. 网络层:epoll检测写事件就绪
   ↓
9. 传输层:dealwithwrite() 发送响应数据
   ↓
10. 应用层:写入访问日志

4,GDB详细调试过程

第一步:发现服务器卡死

# 服务器运行中突然无响应
ps aux | grep webserver
# 发现进程存在但CPU占用率为0%

# 检查网络连接
netstat -tlnp | grep 9006
# 端口在监听,但无法建立新连接

第二步:使用GDB附加进程

# 附加到卡死的进程
sudo gdb -p 12345

# GDB输出
(gdb) Attaching to process 12345
Reading symbols from /path/to/webserver...
0x00007f8b8c0d7 in __pthread_mutex_lock_full () from /lib/x86_64-linux-gnu/libpthread.so.0

第三步:分析所有线程状态

(gdb) info threads
  Id   Target Id         Frame 
* 1    Thread 0x7f8b8c   0x00007f8b8c0d7 in __pthread_mutex_lock_full ()
  2    Thread 0x7f8b8d   0x00007f8b8c0d7 in __pthread_mutex_lock_full ()
  3    Thread 0x7f8b8e   0x00007f8b8c0d7 in __pthread_mutex_lock_full ()
  4    Thread 0x7f8b8f   0x00007f8b8c8a1 in sem_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
  5    Thread 0x7f8b90   0x00007f8b8c1f2 in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6

# 🚨 发现:线程1-3都在mutex_lock,线程4在sem_wait,线程5在epoll_wait

第四步:查看每个线程的详细堆栈

# 查看线程1堆栈
(gdb) thread 1
(gdb) bt
#0  0x00007f8b8c0d7 in __pthread_mutex_lock_full () from /lib/x86_64-linux-gnu/libpthread.so.0
#1  0x0000555555556789 in locker::lock() at lock/locker.h:25
#2  0x0000555555557234 in threadpool<http_conn>::run() at threadpool/threadpool.h:187
#3  0x0000555555557456 in threadpool<http_conn>::worker(void*) at threadpool/threadpool.h:145
#4  0x00007f8b8c0ca6ba in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0

# 查看线程2堆栈 - 同样卡在lock()
(gdb) thread 2
(gdb) bt
#0  0x00007f8b8c0d7 in __pthread_mutex_lock_full ()
#1  0x0000555555556789 in locker::lock() at lock/locker.h:25
#2  0x0000555555557234 in threadpool<http_conn>::run() at threadpool/threadpool.h:187

# 查看线程4堆栈 - 卡在信号量等待
(gdb) thread 4  
(gdb) bt
#0  0x00007f8b8c8a1 in sem_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
#1  0x0000555555556823 in sem::wait() at lock/locker.h:67
#2  0x0000555555557198 in threadpool<http_conn>::run() at threadpool/threadpool.h:185

第五步:检查关键变量状态

# 切换到主线程
(gdb) thread 1

# 查看信号量状态(需要手动计算)
(gdb) p m_queuestat
# 无法直接看到信号量值,需要查看相关变量

# 查看队列状态
(gdb) p m_workqueue.size()
$1 = 0    # 🚨 队列为空!

# 查看锁状态
(gdb) p m_queuelocker
# 显示锁对象信息

# 查看当前正在等待锁的线程数
(gdb) info threads
# 发现多个线程都在等待同一个锁

第六步:分析死锁形成过程

# 使用pstack查看所有线程状态(在另一个终端)
sudo pstack 12345

# 输出显示:
Thread 8 (Thread 0x7f8b8c):
#0  pthread_mutex_lock_full
#1  threadpool<http_conn>::run() (threadpool.h:187)

Thread 7 (Thread 0x7f8b8d):  
#0  pthread_mutex_lock_full
#1  threadpool<http_conn>::run() (threadpool.h:187)

# 🚨 发现问题:多个线程都卡在第187行获取锁

😔二,C 语言基础

1. 指针与数组的本质区别

基本概念对比

  • 指针的本质:指针是一个变量,里面存放的是另一个变量的内存地址
  • 数组名的本质:数组名是一个常量,代表数组第一个元素的地址,不可改变

关键区别展示

  • 可变性差异:
  • char *p = "hello"; 后可以执行 p++,让p指向下一个字符(字符串中的字符,在内存中是连续的,这里 p 只想第一个字符)
  • char arr[] = "hello"; 后不能执行 arr++,因为arr是地址常量
  • 内存分配方式:
  • 指针变量本身占用4/8字节(根据系统位数),指向的内容在别处
  • 数组在栈上连续分配所有元素的空间

函数传参的陷阱

  • 数组退化现象:void func(char arr[]) 实际等价于 void func(char *arr)
  • 为什么会退化:C语言不允许按值传递整个数组,自动转换为指针传递
  • 实际影响:函数内部无法通过sizeof获得原数组大小

2. 动态内存管理的完整机制

三大内存分配函数对比

  • malloc:
    功能:分配指定字节数的,未初始化的,内存
    特点:速度快,但内容不确定(可能是垃圾数据)
    使用场景:知道需要多少内存,不关心初始值时
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        // 分配5个int的内存
        int *arr = (int*)malloc(5 * sizeof(int));
        
        if (arr == NULL) {
            printf("分配失败!\n");
            return -1;
        }
        
        // 查看未初始化的垃圾数据
        printf("malloc未初始化内容: ");
        for (int i = 0; i < 3; i++) {
            printf("%d ", arr[i]);  // 输出随机值
        }
        printf("\n");
        
        // 手动赋值
        for (int i = 0; i < 5; i++) {
            arr[i] = i + 1;
        }
        
        printf("手动初始化后: ");
        for (int i = 0; i < 5; i++) {
            printf("%d ", arr[i]);  // 输出: 1 2 3 4 5
        }
        printf("\n");
        
        free(arr);
        return 0;
    }
  • calloc:
    功能:分配内存并初始化为 0
    特点:相当于malloc + memset,稍慢但更安全
    使用场景:需要干净内存时,如结构体数组初始化
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        // 分配5个int的内存,自动初始化为0
        int *arr = (int*)calloc(5, sizeof(int));
        
        if (arr == NULL) {
            printf("分配失败!\n");
            return -1;
        }
        
        // 查看自动初始化的内容
        printf("calloc自动初始化: ");
        for (int i = 0; i < 5; i++) {
            printf("%d ", arr[i]);  // 输出: 0 0 0 0 0
        }
        printf("\n");
        
        free(arr);
        return 0;
    }
  • realloc:
    功能:重新调整已分配内存的大小
    智能机制:原地扩展或复制到新位置
    注意事项:可能返回不同地址,原指针可能失效
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        // 初始分配3个int
        int *arr = (int*)malloc(3 * sizeof(int));
        if (arr == NULL) return -1;
        
        // 初始化数据
        for (int i = 0; i < 3; i++) {
            arr[i] = i + 1;
        }
        
        printf("原始数组(3个): ");
        for (int i = 0; i < 3; i++) {
            printf("%d ", arr[i]);  // 输出: 1 2 3
        }
        printf("\n");
        
        // 扩展到6个int
        arr = (int*)realloc(arr, 6 * sizeof(int));
        if (arr == NULL) return -1;
        
        // 为新位置赋值
        for (int i = 3; i < 6; i++) {
            arr[i] = i + 1;
        }
        
        printf("扩展后(6个): ");
        for (int i = 0; i < 6; i++) {
            printf("%d ", arr[i]);  // 输出: 1 2 3 4 5 6
        }
        printf("\n");
        
        // 缩减到2个int
        arr = (int*)realloc(arr, 2 * sizeof(int));
        
        printf("缩减后(2个): ");
        for (int i = 0; i < 2; i++) {
            printf("%d ", arr[i]);  // 输出: 1 2
        }
        printf("\n");
        
        free(arr);
        return 0;
    }

内存管理的最佳实践

  • 配对原则:每个malloc必须对应一个free
  • 防野指针:free后立即设置指针为NULL
  • 检查返回值:malloc可能失败返回NULL
  • 内存泄漏检测:使用工具如valgrind定期检查

3. 函数指针的深度应用

概念

文字说明:

Ⅰ 函数指针的标准声明格式为:返回类型 (*指针名)(参数列表)

Ⅱ 赋值时 --> 函数名或取地址符(add 或 &add),等价

Ⅲ 调用时 --> 用 func_ptr(a, b) 或 (*func_ptr)(a, b),等价

代码示例:

#include <stdio.h>

// 函数指针起别名:指向返回void、参数为(int, void*)的函数
typedef void (*bt_callback)(int event, void* data);

// 符合函数指针的实现
void on_event(int event, void* data) {
    printf("Event %d triggered, data = %s\n", event, (char*)data);
}

// 函数指针作为参数传入另一个函数 --> 也就是回调函数
void register_and_trigger(bt_callback cb) {
    char msg[] = "hello";
    // 通过函数指针cb调用回调函数
    cb(100, msg);
}

int main() {
    // 传递回调函数指针
    register_and_trigger(on_event);
    return 0;
}

回调机制

文字说明:

Ⅰ 回调机制常用函数指针作为参数,将处理逻辑“反向”传递给库或框架

Ⅱ 常见用法是用 typedef 定义回调类型,然后在需要时传递和调用

代码示例:

#include <stdio.h>

// 定义回调类型:指向返回void、参数为(int, void*)的函数
typedef void (*bt_callback)(int event, void* data);

// 回调函数实现,符合bt_callback类型
void on_event(int event, void* data) {
    printf("Event %d triggered, data = %s\n", event, (char*)data);
}

// 注册和触发回调
void register_and_trigger(bt_callback cb) {
    char msg[] = "hello";
    // 通过函数指针cb调用回调函数
    cb(100, msg);
}

int main() {
    // 传递回调函数指针
    register_and_trigger(on_event);
    return 0;
}

状态机实现

文字说明:

Ⅰ 状态机常用函数指针数组,将每个状态的处理函数放入数组,通过下标选择调用

代码示例:

#include <stdio.h>

// 定义状态处理函数类型 -- 函数指针
typedef void (*state_func)();

// 各状态对应的处理函数
void state_idle()   { printf("State: IDLE\n"); }
void state_run()    { printf("State: RUN\n"); }
void state_error()  { printf("State: ERROR\n"); }

int main() {
    // 函数指针数组,每个元素指向一个状态处理函数
    state_func states[] = { state_idle, state_run, state_error };
    int current_state = 1; // 0: idle, 1: run, 2: error

    // 通过下标选择并调用对应的状态处理函数
    states[current_state]();
    return 0;
}

C 语言模拟多态

文字说明:

Ⅰ C 语言没有面向对象的多态,但可以用结构体+函数指针模拟

Ⅱ 每个“对象”结构体中包含一个函数指针,指向不同的实现,实现“多态”效果

代码示例:

#include <stdio.h>

// 定义“基类”结构体,包含一个函数指针
typedef struct {
    void (*speak)(); // 指向说话函数
} Animal;

// 不同“子类”的实现
void dog_speak() { printf("Woof!\n"); }
void cat_speak() { printf("Meow!\n"); }

int main() {
    // 创建不同“对象”,分别赋予不同的speak函数
    Animal dog = { dog_speak };
    Animal cat = { cat_speak };

    // 用基类指针数组存储,实现多态
    Animal* animals[] = { &dog, &cat };
    for (int i = 0; i < 2; ++i) {
        animals[i]->speak(); // 多态调用
    }
    return 0;
}

4. 结构体内存对齐

对齐规则详解

  • 成员对齐:每个成员的起始地址必须是该成员大小的整数倍
  • 结构体对齐:整个结构体大小必须是最大成员大小的整数倍
  • 填充字节:编译器自动插入填充字节保证对齐

实际对齐示例

struct example {
    char a;    // 1字节,偏移0
    int b;     // 4字节,偏移4(不是1,因为要4字节对齐)
    char c;    // 1字节,偏移8
    // 总大小12字节(不是10,因为要按最大成员4字节对齐)
};

对齐的影响和控制

  • 性能考虑:对齐提高CPU访问效率,减少内存访问次数
  • 空间权衡:对齐浪费内存空间,但换取访问速度
  • 手动控制:#pragma pack(1) 可以取消对齐
  • 协议解析场景:网络数据包解析时经常需要取消对齐

5. volatile 关键字

volatile 的深层含义

  • 编译器优化问题:

编译器通常会假设变量只被本线程/本程序修改,因此可能将变量值缓存到寄存器,减少内存访问,提高效率。

  • 问题场景:

如果变量可能被外部因素(如硬件、中断、其他线程)修改,编译器的优化会导致程序读到的值不是最新的,产生错误。

  • volatile 作用:

告诉编译器“这个变量随时可能被外部修改”,禁止优化,每次都要从内存读取,不允许用寄存器缓存

三大应用场景

  • 硬件寄存器

访问外设寄存器时,寄存器的值可能随时变化,必须每次都从内存(实际是硬件地址)读取

   volatile unsigned int *gpio_reg = (volatile unsigned int*)0x40001000;
   *gpio_reg = 0x01; // 写寄存器
   unsigned int val = *gpio_reg; // 读寄存器
  • 中断处理

中断服务程序(ISR)可能修改全局变量,主程序必须用 volatile 保证每次都读最新值

   volatile int flag = 0;

   void ISR() { // 中断服务程序
       flag = 1;
   }

   int main() {
       while (!flag) {
           // 等待中断置位
       }
       // 处理中断
   }
  • 多线程共享变量

多线程环境下,某个变量可能被其他线程修改,必须用 volatile 防止编译器优化

   volatile int stop = 0;

   void* worker(void* arg) {
       while (!stop) {
           // 工作线程循环
       }
       return NULL;
   }

   int main() {
       pthread_t tid;
       pthread_create(&tid, NULL, worker, NULL);
       // ... 其他操作 ...
       stop = 1; // 通知线程退出
       pthread_join(tid, NULL);
   }

底层机制

  • 内存屏障:volatile 保证每次访问都从内存读取/写入,不允许编译器优化为寄存器缓存
  • 寄存器缓存失效:禁止将变量值长时间保存在寄存器,确保变量值的“可见性”
  • 编译器行为:强制生成内存访问指令,防止优化

总结

  • volatile 只保证“每次都从内存访问”,不保证原子性,也不保证多核间的同步
  • 在多线程同步时,通常还需要配合互斥锁、原子操作等

6. 位运算在底层控制中的精妙应用

四大基础操作

  • 置位操作:reg |= (1 << n) 将第n位设为1
  • 清位操作:reg &= ~(1 << n) 将第n位设为0
  • 翻转操作:reg ^= (1 << n) 翻转第n位
  • 检测操作:if(reg & (1 << n)) 检查第n位是否为1

高级位操作技巧

  • 多位提取:value = (reg >> start_bit) & ((1 << bit_count) - 1)
  • 掩码应用:#define GPIO_MODE_MASK 0x03 提取特定位段
  • 位域结构:用结构体的位域功能直接操作位

嵌入式开发实战

  • GPIO控制:通过寄存器位控制引脚输入输出状态
  • 中断屏蔽:设置中断使能寄存器的特定位
  • 协议解析:提取数据包头部的控制位信息

7. 联合体的内存共享巧用

内存共享机制

  • 基本原理:所有成员共享同一块内存空间
  • 大小计算:联合体大小等于最大成员的大小
  • 访问方式:可以用不同成员名访问同一块内存

经典应用模式

union data_converter {
    unsigned int full;           // 32位整体访问
    struct {
        unsigned char b0, b1, b2, b3;  // 字节级访问
    } bytes;
};

实际应用价值

  • 字节序转换:在大端和小端之间转换数据
  • 协议解析:将接收到的字节流解释为不同的数据结构
  • 内存节省:在资源受限的嵌入式系统中节省内存

8. 变量作用域和生命周期的精确控制

三种静态变量对比

  • 全局变量:
  •         作用域:整个程序
  •         链接性:外部链接,可被其他文件访问
  •         生命周期:程序运行期间
  • 静态全局变量:
  •         作用域:当前文件
  •         链接性:内部链接,其他文件无法访问
  •         用途:文件内部的"私有"全局变量
  • 静态局部变量:
  •         作用域:函数内部
  •         生命周期:程序运行期间(不随函数结束而销毁)
  •         用途:保持函数调用间的状态

内存分布差异

  • 存储位置:静态变量存储在数据段,不在栈中
  • 初始化时机:程序启动时初始化,只初始化一次
  • 默认初始化:未显式初始化的静态变量自动初始化为0

9. 字符串操作的安全防护

危险函数的安全替代

  • strcpy → strncpy:
  • 危险:strcpy(dest, src) 不检查目标缓冲区大小
  • 安全:strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0';
  • strcat → strncat:
  • 危险:字符串连接可能溢出
  • 安全:使用strncat并确保总长度不超限
  • sprintf → snprintf:
  • 危险:格式化输出可能溢出缓冲区
  • 安全:snprintf限制最大输出长度

长度计算的陷阱

  • strlen vs sizeof:
  • strlen计算字符串长度(不包括'\0')
  • sizeof计算数组大小(包括'\0')
  • 动态字符串处理:处理用户输入时必须验证长度

缓冲区溢出防护

  • 边界检查:始终检查输入长度是否超过缓冲区
  • 安全编程习惯:预留足够空间,添加结束符
  • 输入验证:对所有外部输入进行长度和内容验证

10. 预处理器的高级应用技巧

宏定义的安全写法

  • 参数加括号:#define MAX(a,b) ((a)>(b)?(a):(b)) 避免优先级问题
  • 多语句宏:使用do-while(0)结构包装多语句宏
  • 副作用防范:避免宏参数被多次计算

条件编译的实战应用

  • 平台适配:
      #ifdef _WIN32
          // Windows特定代码
      #elif defined(__linux__)  
          // Linux特定代码
      #endif
  • 调试开关:通过宏控制调试信息的编译
  • 功能裁剪:根据需求编译不同功能模块

高级预处理技巧

  • 字符串化:# 将宏参数转换为字符串
  • 标记连接:## 连接两个标记生成新标识符
  • 包含保护:#ifndef/#define/#endif 防止头文件重复包含

#pragma

作用

  • #pragma 是编译器指令,用于向编译器传递特定的编译信息,具体行为依赖编译器实现
  • 常见用途:控制警告、结构体对齐、优化开关、头文件只包含一次

常用

Ⅰ 结构体对齐

#pragma pack(1) // 结构体按1字节对齐
struct S {
    char a;
    int b;
};
#pragma pack()  // 恢复默认对齐

Ⅱ 只包含一次(类似 include guard)

#pragma once
// 头文件内容

Ⅲ 控制编译警告(MSVC 为例)

#pragma warning(disable:4996) // 禁用4996号警告

Ⅳ GCC 优化控制

#pragma GCC optimize("O3")

宏定义的安全写法

Ⅰ 参数加括号

#define MAX(a, b) ((a) > (b) ? (a) : (b))

Ⅱ 多语句宏

#define SWAP(a, b) do { \
    int tmp = (a);      \
    (a) = (b);          \
    (b) = tmp;          \
} while(0)

Ⅲ 副作用防范

避免宏参数被多次求值(如 MAX(i++, j++)),可用内联函数替代,或在宏内部用临时变量(但 C 里不易实现)

条件编译

Ⅰ 平台适配

#ifdef _WIN32
    // Windows 特定代码
#elif defined(__linux__)
    // Linux 特定代码
#endif

Ⅱ 调试开关

#ifdef DEBUG
    printf("Debug info: %d\n", x);
#endif

Ⅲ 功能裁剪

#define FEATURE_X

#ifdef FEATURE_X
    // 编译 Feature X 相关代码
#endif
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千帐灯无此声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值