MTK外包面经



交叉编译是什么?在嵌入式中使用交叉编译有什么好处?
交叉编译是一种在一个平台(主机平台)上编译出能在另一个不同架构或操作系统的平台(目标平台)上运行的程序的技术。这里的 “平台” 通常指硬件架构(如 x86、ARM、RISC-V)和操作系统(如 Linux、Windows、嵌入式实时系统)的组合。
举个例子:
- 你在 x86 架构的 Linux 电脑(主机) 上编写代码,编译出能在 ARM 架构的嵌入式设备(目标平台,如树莓派、路由器) 上运行的程序,这就是交叉编译。
- 反之,如果在目标平台上直接编译(比如在树莓派上编译自身运行的程序),则称为 “本地编译”。
好处:
嵌入式设备(如单片机、传感器、智能手表、路由器等)通常具有 CPU 性能弱、内存小、存储有限 的特点,根本无法运行复杂的编译工具链(如 GCC、Clang)可以通过交叉编译可以解决该问题
嵌入式开发中,目标设备可能种类繁多(如 ARM、RISC-V、MIPS 等不同架构),且调试接口各异(如 JTAG、UART):
- 开发者无需在每个目标设备上配置编译环境,只需在主机上安装对应架构的交叉编译工具链(如
arm-linux-gnueabihf-gcc),即可统一管理多平台开发。 - 避免了在嵌入式设备上操作命令行、处理依赖冲突等繁琐工作,减少人为错误。
KMP模式匹配算法
KMP 模式匹配算法是一种高效的字符串匹配算法,由 Knuth、Morris 和 Pratt 三人共同提出,其核心思想是利用已匹配的信息避免不必要的字符比较,从而将时间复杂度从朴素算法的 O (n*m)(n 为主串长度,m 为模式串长度)优化到 O (n+m)。
一、问题背景:字符串匹配的痛点
字符串匹配的目标是:在主串(如"ABCABCDABABCDABCDABDE")中查找模式串(如"ABCDABD")的位置。朴素算法的问题在于:当匹配失败时,主串指针会回溯到起始位置的下一位,导致大量重复比较。例如:
- 主串:
A B C A B C D... - 模式串:
A B C D...前 3 位匹配成功,第 4 位失败(主串是A,模式串是D)。朴素算法会让主串指针从第 4 位回溯到第 2 位,重新比较,效率低下。
KMP 的核心优化:不回溯主串,只移动模式串
KMP 的关键是通过模式串自身的结构,计算出一个 “部分匹配表”(Partial Match Table,简称 PMT,也叫前缀函数),利用它来确定匹配失败时模式串应该向右移动多少位,从而避免主串指针回溯。
二、部分匹配表(PMT)的定义
对于模式串的每个位置i(从 0 开始),PMT [i] 表示:模式串中前i+1个字符组成的子串中,最长的 “前缀” 与 “后缀” 相等的长度。
- 前缀:不包含最后一个字符的所有头部子串(如
"ABCD"的前缀为"A"、"AB"、"ABC")。 - 后缀:不包含第一个字符的所有尾部子串(如
"ABCD"的后缀为"D"、"CD"、"BCD")。
示例:模式串"ABCDABD"的 PMT 表
模式串索引i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 模式串字符 | A | B | C | D | A | B | D |
| PMT[i] | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
i=0(子串"A"):无前缀 / 后缀,PMT [0]=0。i=4(子串"ABCDA"):最长相等前缀"A"与后缀"A",长度 1,故 PMT [4]=1。i=5(子串"ABCDAB"):最长相等前缀"AB"与后缀"AB",长度 2,故 PMT [5]=2。
三、KMP 算法的匹配过程
- 预处理:计算模式串的 PMT 表,得到一个与模式串等长的数组(通常将 PMT 优化为 “next 数组”,方便计算移动距离)。
- 匹配阶段:
- 用
i指向主串当前位置,j指向模式串当前位置(初始均为 0)。 - 若主串
s[i] == 模式串p[j],则i++、j++,继续匹配。 - 若不相等:
- 若
j > 0,则j = next[j-1](根据 PMT 调整模式串位置,避免主串回溯)。 - 若
j == 0,则i++(主串后移一位,模式串从头开始)。
- 若
- 当
j == 模式串长度时,匹配成功,返回起始位置i - j。
- 用
示例:主串"ABCABCDABABCDABCDABDE"匹配模式串"ABCDABD"
- 当匹配到
i=6、j=6时,主串s[6] = 'C',模式串p[6] = 'D',不匹配。 - 此时
j=6,查 PMT [j-1] = PMT [5] = 2,故j = 2(模式串右移6-2=4位)。 - 主串
i不回溯,继续比较s[6]与p[2]('C' == 'C'),后续匹配顺利完成。
可以去B站看看该算法的视频动画, 能加深理解
快排
快速排序(Quick Sort)是一种高效的排序算法,由计算机科学家 Tony Hoare 在 1960 年提出。它的核心思想是 “分而治之”,通过选择一个 “基准元素” 将数组分为两部分,使得左半部分的元素都小于等于基准,右半部分的元素都大于等于基准,然后递归地对两部分进行排序。
快排的核心步骤
- 选择基准(Pivot):从数组中挑选一个元素作为基准(通常选第一个、最后一个或中间元素,也可随机选择)。
- 分区(Partition):将数组重新排列,所有比基准小的元素移到基准左边,比基准大的元素移到基准右边(相等的元素可放任意一边)。此时基准的位置已确定(最终排序后的位置)。
- 递归排序:对基准左右两侧的子数组分别重复上述步骤,直到子数组长度为 0 或 1(天然有序)。
在视频流中AvPacket的定义和存取方式
在 FFmpeg 多媒体处理框架中,AVPacket是用于存储编码后的音视频数据包的核心结构体,是音视频流处理(如读取、编码、解码、封装、传输)的关键数据载体。它主要用于在不同模块(如解复用器、编码器、解码器、复用器)之间传递编码后的原始数据(如 H.264/H.265 视频帧、AAC 音频帧)。
一、AVPacket的定义与核心成员
AVPacket的定义位于 FFmpeg 的libavcodec/avpacket.h头文件中,其核心成员如下(简化版):
typedef struct AVPacket {uint8_t *data; // 存储编码后的音视频数据(原始字节流)int size; // data的大小(字节数)int64_t pts; // 显示时间戳(Presentation Time Stamp,单位:流的时间基)int64_t dts; // 解码时间戳(Decoding Time Stamp,单位:流的时间基)int stream_index; // 该数据包所属的流索引(如视频流、音频流)int flags; // 标志位(如AV_PKT_FLAG_KEY表示关键帧)AVPacketSideData *side_data; // 附加数据(如字幕、加密信息等)int side_data_elems; // 附加数据的数量int64_t duration; // 数据包的持续时间(单位:流的时间基)void *opaque; // 自定义私有数据// ... 其他辅助成员
} AVPacket;
关键成员说明:
data和size:最核心的字段,存储编码后的原始数据(如视频的 NAL 单元、音频的帧数据)。pts和dts:时间戳,用于同步音视频播放顺序(视频可能存在 B 帧,导致 DTS≠PTS)。stream_index:标识该数据包属于哪个流(一个媒体文件可能包含多个流,如视频流、音频流、字幕流)。flags:重要标志,例如AV_PKT_FLAG_KEY表示该数据包是关键帧(可独立解码),非关键帧(如 P 帧、B 帧)依赖其他帧。
二、AVPacket的生命周期与存取方式
AVPacket的使用需严格遵循 FFmpeg 的内存管理规则,避免内存泄漏或数据错误。核心操作包括初始化、填充数据、传递、释放等。
1. 初始化与分配
使用前需初始化AVPacket,通常有两种方式:
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>// 方式1:静态初始化(栈上分配)
AVPacket pkt;
av_init_packet(&pkt); // 初始化成员(data=NULL, size=0, 其他字段置默认值)// 方式2:动态分配(堆上分配,需手动释放)
AVPacket *pkt = av_packet_alloc(); // 分配并初始化,返回指针
2. 填充数据(获取数据包)
AVPacket的数据通常来自解复用器(demuxer) 或编码器(encoder):
从媒体文件中读取(解复用):通过
av_read_frame()从AVFormatContext中读取数据包:AVFormatContext *fmt_ctx = avformat_alloc_context(); avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL); // 打开媒体文件 avformat_find_stream_info(fmt_ctx, NULL); // 获取流信息AVPacket *pkt = av_packet_alloc(); while (av_read_frame(fmt_ctx, pkt) >= 0) { // 循环读取数据包// pkt->stream_index 标识当前包属于哪个流if (pkt->stream_index == video_stream_index) {// 处理视频包(如解码、存储)} else if (pkt->stream_index == audio_stream_index) {// 处理音频包}av_packet_unref(pkt); // 重置数据包(重要:避免内存泄漏) }从编码器输出(编码生成):编码器编码完成后,通过
avcodec_receive_packet()获取编码后的AVPacket:AVCodecContext *enc_ctx; // 已初始化的编码器上下文 AVPacket *pkt = av_packet_alloc(); // 向编码器发送原始帧(avcodec_send_frame())后,接收编码包 while (avcodec_receive_packet(enc_ctx, pkt) == 0) {// 处理编码后的数据包(如写入文件、传输)av_packet_unref(pkt); }
3. 数据访问与修改
直接通过data和size访问编码数据,例如解析视频 NAL 单元:
// 假设pkt是一个H.264视频包,data中存储NAL单元
if (pkt->stream_index == video_stream_index && pkt->size > 0) {uint8_t *data = pkt->data;int size = pkt->size;// 遍历NAL单元(H.264 NAL以0x000001或0x00000001为起始码)// ...(解析逻辑)
}
修改数据时需注意内存管理,若需替换data,需先释放原有数据:
av_packet_unref(pkt); // 释放原有data
pkt->data = new_data; // 指向新数据
pkt->size = new_size; // 设置新大小
4. 释放与重置
AVPacket使用完后必须释放资源,核心函数:
av_packet_unref(pkt):重置数据包,释放data、side_data等动态分配的内存,但不释放pkt本身(适用于循环复用的情况)。av_packet_free(&pkt):释放pkt指向的结构体及内部资源(适用于动态分配的AVPacket*)。
// 静态初始化的pkt:使用后重置
av_init_packet(&pkt); // 或 av_packet_unref(&pkt);// 动态分配的pkt:使用后释放
av_packet_free(&pkt); // pkt会被置为NULL
三、注意事项
- 引用计数:
AVPacket内部通过引用计数管理data的生命周期(类似AVFrame),av_packet_ref(pkt, src)可创建引用,av_packet_unref()减少引用计数,计数为 0 时释放内存。 - 时间基转换:
pts和dts的单位是所属流的时间基(AVStream->time_base),需转换为标准时间(如秒)时,需用av_q2d(stream->time_base) * pkt->pts。 - 关键帧判断:通过
pkt->flags & AV_PKT_FLAG_KEY判断是否为关键帧,对视频解码和 Seek 操作至关重要。 - 线程安全:
AVPacket本身不是线程安全的,多线程操作需加锁。
总结
AVPacket是 FFmpeg 中编码数据的 “容器”,承载了音视频流的核心信息(数据、时间戳、流索引等)。其存取方式需遵循 FFmpeg 的内存管理规则,核心流程为:初始化→填充数据(解复用 / 编码)→访问 / 处理→释放 / 重置。理解AVPacket的使用是掌握 FFmpeg 音视频处理的基础。
在实现视频回放功能时, 在拉流过程中应该边拉流边存储吗?
在实现回放功能时,“边拉流边存储” 是一种常见且合理的方案,但需根据具体场景(如实时性要求、存储资源、回放需求)灵活设计。以下从技术逻辑、优缺点和适用场景展开分析:
一、为什么需要 “边拉流边存储”?
回放功能的核心是 “能够重现历史流数据”,而流数据(如 RTSP/RTMP 直播流、传感器实时数据流)具有实时性、连续性和一次性的特点(不存储则会丢失)。因此,若需要回放,必须在拉流的同时对数据进行持久化存储,否则后续无法追溯历史内容。
二、边拉流边存储的实现逻辑
典型流程如下:
- 拉流模块:通过网络协议(如 RTSP、HTTP-FLV)从源端(摄像头、服务器)拉取实时流数据(通常是编码后的
AVPacket或原始字节流)。 - 分流处理:拉取的流数据分为两路:
- 实时处理:直接传给解码 / 渲染模块,实现实时预览。
- 存储处理:按固定格式(如 MP4、TS、自定义分片文件)写入本地磁盘或数据库。
- 存储格式设计:
- 视频流通常按时间分片存储(如每 5 分钟一个 MP4 文件),便于后续按时间范围检索。
- 需记录每个分片的时间戳(开始 / 结束时间),作为回放时定位的索引。
- 回放时:根据用户选择的时间范围,从存储中读取对应分片文件,重新组装为流并播放。
三、优点
- 保证回放数据的完整性:实时存储避免流数据丢失,确保任何时间点的内容都可回放。
- 低延迟实时预览:拉流后可同时满足 “实时看” 和 “事后存”,无需二次拉流。
- 灵活的回放定位:通过时间戳索引,可快速定位到任意时刻的历史内容(类似视频播放器的进度条)。
- 适应高并发场景:若回放请求较多,可直接从存储读取,避免反复请求源端流(减轻源端压力)。
四、潜在问题与解决方案
存储资源消耗大:
- 视频流(尤其是高清)占用空间大(如 1080P H.264 流约 100-300MB / 小时)。
- 解决方案:设置存储生命周期(如保留 7 天),自动清理过期文件;采用压缩编码(如 H.265)减少体积。
写入性能瓶颈:
- 高码率流(如 4K 视频)可能导致磁盘写入速度不足,出现卡顿或丢帧。
- 解决方案:使用 SSD 提升写入速度;采用缓存机制(如先写入内存缓冲区,再异步刷盘);降低存储码率(与实时预览码率分离)。
时间同步问题:
- 存储的流数据时间戳若与实际时间不同步,会导致回放定位不准。
- 解决方案:拉流时记录精确的接收时间戳(而非流内自带的相对时间),作为存储索引的基准。
格式兼容性:
- 直接存储原始流(如 RTSP 的 RTP 包)可能不便于回放(需专用解析器)。
- 解决方案:存储为通用容器格式(如 MP4、TS),支持主流播放器直接播放。
五、是否必须 “边拉边存”?
并非所有场景都需要:
- 不需要:若回放仅针对 “最近的短时间内容”(如直播回放延迟 10 分钟内),可临时缓存到内存(无需持久化),但风险是断电或崩溃会丢失数据。
- 必须:若需要长期回放(如监控录像需保存 30 天)、支持任意时间点回溯,或流源不可重复获取(如一次性直播),则必须边拉边存。
六、总结
在大多数需要可靠回放的场景中,“边拉流边存储” 是最优选择,它能平衡实时性与可追溯性,且通过合理的存储策略(分片、生命周期管理、缓存)可规避资源消耗问题。核心是设计好存储格式和索引机制,确保回放时能高效定位和读取历史数据。
随机 crash
“随机 crash”(随机崩溃)指的是程序或系统在无固定规律、不可预测的情况下突然停止运行的现象。其核心特点是:同样的操作步骤,有时正常运行,有时突然崩溃;崩溃时间、场景不固定,难以通过简单复现来定位原因,是开发和调试中非常棘手的问题。
随机 crash 的常见表现
- 程序突然退出(无任何提示)或弹出错误窗口(如 “程序已停止工作”“Segmentation Fault”)。
- 崩溃时可能伴随日志输出(如核心转储
core dump、系统日志dmesg中的错误),但也可能无任何日志。 - 在不同环境(如不同硬件、系统版本)或不同负载下,崩溃概率可能不同(如高并发时更易触发)。
随机 crash 的典型原因
随机 crash 通常与内存操作不当、并发竞争、资源竞争等 “不确定性” 问题相关,具体包括:
内存访问错误
- 野指针:访问已释放的内存(如
free后未置空的指针,再次访问时该内存可能已被其他模块占用,数据随机变化)。 - 缓冲区溢出:写入数据超过数组 / 缓冲区的边界,覆盖相邻内存(可能破坏函数栈、堆结构,导致程序执行流紊乱,崩溃时机取决于被覆盖的数据是否关键)。
- 内存泄漏累积:长期运行后内存耗尽(OOM),但崩溃时间取决于内存泄漏速度和系统可用内存,表现为随机崩溃。
例:某程序在循环中动态分配内存
malloc(1024)但未释放,短时间运行正常,几小时后因内存耗尽随机崩溃。- 野指针:访问已释放的内存(如
多线程 / 并发竞争
- 数据竞争(Data Race):多个线程同时读写共享变量,且未加锁保护。例如,线程 A 正在修改变量
count,线程 B 同时读取count,可能导致count值异常(如读取到中间值),进而引发逻辑错误(如数组越界),崩溃与否取决于线程调度顺序。 - 死锁 / 活锁:线程因争夺资源陷入无限等待,虽不直接崩溃,但可能导致程序无响应,被系统强制终止(表现为 “随机” 退出)。
- 线程安全问题:调用非线程安全的函数(如 C 标准库的
strtok、rand),多线程同时操作时可能导致内部状态错乱,触发不可预测的崩溃。
例:两个线程同时向同一个链表插入节点,未加互斥锁,可能导致链表指针断裂(如
next指针被同时修改),多数时候正常,偶尔因调度冲突崩溃。- 数据竞争(Data Race):多个线程同时读写共享变量,且未加锁保护。例如,线程 A 正在修改变量
资源竞争与耗尽
- 文件描述符 / 句柄泄漏:频繁打开文件、网络连接但未关闭,导致系统资源耗尽(如 Linux 下默认文件描述符上限为 1024),后续
open/socket调用失败,若程序未处理错误,可能访问无效句柄崩溃,崩溃时机取决于资源消耗速度。 - 动态链接库(DLL/SO)冲突:程序加载多个版本不兼容的库(如不同版本的
libc),或库文件被动态替换(如更新软件时),导致符号解析错误,崩溃时机与库加载顺序相关。
- 文件描述符 / 句柄泄漏:频繁打开文件、网络连接但未关闭,导致系统资源耗尽(如 Linux 下默认文件描述符上限为 1024),后续
硬件 / 环境因素
- 内存硬件故障:物理内存存在坏块,程序运行时恰好访问到该区域,导致随机崩溃(可通过
memtest工具检测)。 - CPU 缓存 / 指令重排:多线程环境下,CPU 指令重排可能导致共享变量的可见性问题(未使用
volatile或内存屏障),引发逻辑错误,崩溃概率与 CPU 调度相关。 - 系统资源限制:如栈空间不足(递归过深未限制)、进程数 / 线程数达到系统上限,触发崩溃的时机取决于具体操作路径。
- 内存硬件故障:物理内存存在坏块,程序运行时恰好访问到该区域,导致随机崩溃(可通过
如何调试随机 crash?
由于随机性强,调试难度高,常用手段包括:
- 开启核心转储(core dump):配置系统生成崩溃时的内存快照(如
ulimit -c unlimited),事后用gdb分析core文件,定位崩溃时的函数调用栈和变量状态。 - 添加详细日志:在关键操作(内存分配、线程同步、资源访问)处打印日志,记录时间戳、变量值、线程 ID,通过日志回溯崩溃前的异常状态。
- 使用工具检测:
- 内存问题:
valgrind(检测内存泄漏、野指针)、AddressSanitizer(ASAN,检测缓冲区溢出)。 - 并发问题:
ThreadSanitizer(TSAN,检测数据竞争)、Helgrind(检测死锁)。
- 内存问题:
- 压力测试与复现:通过脚本模拟高并发、高负载场景(如循环执行操作 10 万次),提高崩溃概率,缩小排查范围。
总结
随机 crash 本质是程序中隐藏的 “不确定性缺陷”(如内存错误、并发竞争)在特定条件下被触发的结果。其随机性源于硬件调度、线程执行顺序、资源分配等不可控因素,调试时需结合工具、日志和压力测试,从 “不确定性” 中寻找 “确定性” 的规律。
IPC进程间通信有哪些
进程间通信(IPC,Inter-Process Communication)是指不同进程之间交换数据、同步操作或传递信号的机制。由于进程拥有独立的内存空间,无法直接访问彼此的地址空间,因此需要通过操作系统提供的特定接口实现通信。以下是常见的进程间通信方式,按适用场景和特性分类:
一、基于内存的通信(高效,适用于同一主机)
1. 共享内存(Shared Memory)
- 原理:多个进程通过映射同一块物理内存到各自的虚拟地址空间,直接读写这块内存实现通信,无需数据拷贝。
- 特点:
- 速度最快(无内核中转,直接内存访问)。
- 需配合同步机制(如信号量、互斥锁)防止数据竞争。
- 适用场景:高频、大数据量通信(如视频处理、实时数据共享)。
- 实现:Linux 下通过
shmget、shmat系统调用;Windows 下通过CreateFileMapping。
2. 内存映射文件(Memory-Mapped Files)
- 原理:将磁盘文件映射到进程的虚拟内存,进程通过读写内存间接操作文件,多个进程映射同一文件即可共享数据。
- 特点:
- 数据持久化(同步到磁盘),适合需要持久化的共享数据。
- 速度接近共享内存,适用于大文件共享。
- 适用场景:跨进程共享大型数据集(如数据库缓存、日志文件共享)。
- 实现:Linux 下通过
mmap;Windows 下通过MapViewOfFile。
二、基于消息的通信(灵活,支持不同粒度数据)
1. 管道(Pipe)
- 原理:内核中的一个缓冲区,通过文件描述符操作,数据单向流动(半双工),只能在父子进程或兄弟进程间使用(基于 fork 继承)。
- 特点:
- 简单易用,适用于单向、流式数据传递。
- 容量有限(通常几 KB),满了会阻塞写操作。
- 适用场景:命令行管道(如
ls | grep)、父子进程间简单数据传递。 - 实现:Linux/UNIX 下通过
pipe系统调用。
2. 命名管道(Named Pipe / FIFO)
- 原理:与管道类似,但通过文件系统中的路径名标识,允许任意进程(无亲缘关系)通过路径访问,支持双向通信(需两个 FIFO)。
- 特点:
- 突破管道的亲缘关系限制,可在任意进程间使用。
- 数据在内存中传递,不持久化。
- 适用场景:同一主机上无亲缘关系的进程通信(如客户端 - 服务器模型)。
- 实现:Linux 下通过
mkfifo创建;Windows 下称为 “命名管道”(CreateNamedPipe)。
3. 消息队列(Message Queue)
- 原理:内核维护的一个消息链表,进程可按类型发送 / 接收消息(结构化数据),支持异步通信。
- 特点:
- 数据有格式(消息类型 + payload),可按类型读取,无需顺序接收。
- 生命周期独立于进程(进程退出后消息可保留)。
- 适用场景:需要按优先级或类型处理的消息(如任务调度、日志收集)。
- 实现:Linux 下通过
msgget、msgsnd、msgrcv;Windows 下通过 “消息队列” API。
三、基于信号的通信(简单通知,适用于事件触发)
信号(Signal)
- 原理:操作系统向进程发送的异步通知(如
SIGINT中断、SIGTERM终止),进程可注册信号处理函数响应。 - 特点:
- 传递的是 “事件” 而非数据,只能携带有限信息(信号编号)。
- 处理机制简单,适合紧急事件(如程序异常终止、用户中断)。
- 适用场景:进程间简单通知(如子进程退出通知父进程、超时提醒)。
- 实现:Linux 下通过
kill发送信号,signal或sigaction注册处理函数。
四、基于同步的通信(协调进程执行顺序)
1. 信号量(Semaphore)
- 原理:一个计数器,用于控制多个进程对共享资源的访问,通过
P(减 1,资源占用)和V(加 1,资源释放)操作实现同步。 - 特点:
- 主要用于同步(防止竞态条件),而非传递数据。
- 支持多个进程同时访问有限资源(如限制 5 个进程同时读写文件)。
- 适用场景:共享资源的并发控制(如数据库连接池、多进程文件写入)。
- 实现:Linux 下通过
semget、semop;Windows 下通过CreateSemaphore。
2. 互斥锁(Mutex)
- 原理:特殊的信号量(值只能为 0 或 1),确保同一时间只有一个进程访问共享资源(互斥访问)。
- 特点:
- 比信号量更简单,专用于 “排他性” 访问控制。
- 支持 “所有权”(谁加锁谁解锁),防止误操作。
- 适用场景:保护临界区(如单例模式初始化、共享内存写操作)。
- 实现:Linux 下通过
pthread_mutex_t(线程互斥锁,扩展到进程需共享内存);Windows 下通过CreateMutex。
五、网络通信(跨主机,基于网络协议)
1. Socket(套接字)
- 原理:通过网络协议(TCP/UDP)实现进程通信,可在同一主机(
localhost)或不同主机间使用。 - 特点:
- 通用性强,支持跨主机、跨平台通信。
- TCP 提供可靠的字节流,UDP 提供不可靠的 datagram。
- 适用场景:客户端 - 服务器模型(如 Web 服务、分布式系统)、跨主机进程通信。
- 实现:几乎所有操作系统都支持(
socket、bind、connect等系统调用)。
2. RPC(远程过程调用)
- 原理:通过网络调用远程进程的函数,屏蔽网络细节,让跨进程调用像本地函数一样简单。
- 特点:
- 基于 Socket 封装,简化跨主机通信编程。
- 支持同步 / 异步调用,常用于分布式系统。
- 适用场景:分布式服务(如微服务间调用、分布式计算)。
- 实现:gRPC、Thrift、XML-RPC 等框架。
总结:选择依据
| 需求场景 | 推荐方式 | 核心优势 |
|---|---|---|
| 同一主机,高频大数据量 | 共享内存 / 内存映射文件 | 速度最快,无数据拷贝 |
| 同一主机,简单数据传递 | 命名管道 / 消息队列 | 易用性好,支持无亲缘关系进程 |
| 跨主机通信 | Socket / RPC | 通用性强,支持网络传输 |
| 进程同步 / 资源控制 | 信号量 / 互斥锁 | 防止竞态条件,保证安全性 |
| 简单事件通知 | 信号 | 轻量,适合紧急事件 |
实际开发中,常组合使用多种 IPC(如共享内存 + 信号量:共享内存传递数据,信号量控制访问)。
STL容器有哪些, 它们各自有哪些适用场景?
STL(Standard Template Library,标准模板库)提供了多种容器(Container),用于存储和管理数据,每种容器有其独特的底层结构和适用场景。以下是常用 STL 容器的分类、特性及典型应用场景:
一、序列容器(Sequential Containers)
按插入顺序存储元素,元素位置与插入顺序相关,不自动排序。
| 容器 | 底层结构 | 核心特性 | 时间复杂度(增删查) | 适用场景 |
|---|---|---|---|---|
vector | 动态数组 | 内存连续,支持随机访问;尾部增删高效,中间 / 头部增删需移动元素。 | 随机访问:O (1);尾部增删:O (1);中间增删:O (n) | 需频繁随机访问、尾部插入,且元素数量变化可控(如存储列表、缓存数据)。 |
deque | 双端队列(分段连续) | 支持双端高效增删,随机访问效率略低于vector;内存分配更灵活(避免大内存块)。 | 随机访问:O (1);双端增删:O (1);中间增删:O (n) | 需频繁在头部和尾部操作(如队列、栈的混合场景,滑动窗口缓冲区)。 |
list | 双向链表 | 内存不连续,不支持随机访问;任意位置增删仅需修改指针,效率高。 | 随机访问:O (n);任意增删:O (1)(已知位置时) | 需频繁在中间插入 / 删除(如链表式数据、频繁重组的列表)。 |
forward_list | 单向链表 | 仅支持单向遍历,内存开销比list小;尾部增删效率低(需遍历)。 | 随机访问:O (n);头部增删:O (1);其他:O (n) | 内存受限,且仅需单向遍历、频繁头部操作(如轻量级链表、简单队列)。 |
array | 静态数组 | 大小固定,编译期确定;内存连续,随机访问高效,不支持动态扩容。 | 随机访问:O (1);增删:不支持(大小固定) | 存储固定长度的数据(如坐标、配置参数),需避免动态内存分配的场景。 |
二、关联容器(Associative Containers)
元素按键(Key)排序存储,支持快速查找,底层多为红黑树(平衡二叉树)。
| 容器 | 底层结构 | 核心特性 | 时间复杂度(增删查) | 适用场景 |
|---|---|---|---|---|
set | 红黑树 | 存储唯一键(Key 即 Value),自动按键排序;不允许重复元素。 | 增删查:O (log n) | 需去重并排序的场景(如存储唯一 ID、排序的标签集合)。 |
multiset | 红黑树 | 与set类似,但允许重复元素(键可重复)。 | 增删查:O (log n) | 需排序且允许重复的场景(如统计频率、存储多个相同优先级的任务)。 |
map | 红黑树 | 存储键值对(Key-Value),键唯一且排序;通过键快速查找值。 | 增删查:O (log n) | 需键值映射且按键排序的场景(如字典、配置表、用户信息索引)。 |
multimap | 红黑树 | 与map类似,键可重复(一个键对应多个值)。 | 增删查:O (log n) | 一对多映射且需排序(如按类别分组的列表、多值索引)。 |
三、无序关联容器(Unordered Associative Containers)
元素无序存储,底层为哈希表,查找效率极高(平均 O (1))。
| 容器 | 底层结构 | 核心特性 | 时间复杂度(平均) | 适用场景 |
|---|---|---|---|---|
unordered_set | 哈希表 | 存储唯一键,无序;哈希函数决定存储位置,查找速度快于set。 | 增删查:O (1);最坏:O (n) | 需快速去重但无需排序(如判断元素是否存在、黑名单过滤)。 |
unordered_multiset | 哈希表 | 与unordered_set类似,允许重复键。 | 增删查:O (1);最坏:O (n) | 需快速插入重复元素且无需排序(如统计词频、临时缓存多个相同值)。 |
unordered_map | 哈希表 | 存储键值对,键唯一且无序;查找速度快于map,但内存开销更大。 | 增删查:O (1);最坏:O (n) | 需高效键值查找且无需排序(如缓存、哈希表索引、快速数据映射)。 |
unordered_multimap | 哈希表 | 与unordered_map类似,键可重复。 | 增删查:O (1);最坏:O (n) | 一对多映射且需快速查找(如日志按类型分组、多值哈希索引)。 |
四、容器适配器(Container Adapters)
基于其他容器封装的特殊接口,不直接提供迭代器,专注特定功能。
| 容器 | 底层默认容器 | 核心特性 | 适用场景 |
|---|---|---|---|
stack | deque | 后进先出(LIFO),仅支持栈顶操作(push/pop/top)。 | 需栈结构的场景(如表达式求值、递归模拟、深度优先搜索 DFS)。 |
queue | deque | 先进先出(FIFO),仅支持队尾插入、队头删除(push/pop/front/back)。 | 需队列结构的场景(如任务调度、广度优先搜索 BFS、消息队列)。 |
priority_queue | vector | 优先级队列,元素按优先级排序(默认最大堆),每次弹出优先级最高的元素。 | 需按优先级处理元素(如任务调度器、最大 / 最小值实时获取)。 |
选择容器的核心依据
- 是否需要排序:需排序用
set/map;无需排序且需快速查找用无序容器(unordered_*)。 - 访问方式:随机访问优先
vector/deque;频繁增删中间元素用list。 - 性能需求:高频查找用哈希容器(
unordered_*);内存敏感用forward_list/array。 - 功能场景:栈 / 队列操作选适配器;键值映射用
map/unordered_map。
例如:
- 存储用户 ID 并去重,且需按 ID 排序 →
set; - 存储学生成绩(学号→分数),需快速查询 →
unordered_map; - 实现一个 undo/redo 功能(后进先出) →
stack; - 存储动态增长的日志列表,需频繁遍历和尾部添加 →
vector。
