嵌入式C语言笔试面试高频考点:进程线程、TCP握手与Linux内存外设
简介面向准备 Linux 嵌入式开发岗位笔试与面试的求职者这份资料围绕 C 语言与操作系统核心考点做集中梳理适合用来在投递前查漏补缺、面试前快速复盘。压缩包仅含 1 个 docx 文档大小约 75KB内容以问答形式组织便于按主题检索和打印批注。文档覆盖进程与线程概念、多进程多线程优缺点及选用场景、状态转换、父子进程关系、进程与线程通信方式对比并延伸到同步方法、锁的种类、死锁成因与规避策略等高频问题。已有 841 人学习可帮助读者把零散知识点串成面试表达框架理解 fork 返回值、共享内存同步、互斥锁与读写锁差异等易错点也能作为嵌入式 C 语言基础复习清单使用。1. 分数分布真正拉开差距的是“说清楚”并非写代码嵌入式 C 语言笔试的分数分布很反直觉真正要写代码的题通常只占两三成剩下七八成是“说清楚”的题——进程和线程有什么区别、TCP 为什么可靠、堆和栈怎么区分。linux 嵌入式 c语言 这套知识骨架考的是完整度而工程能力通常不是重点概念能不能一句话说准机制能不能按顺序讲完边界条件能不能补一句。某高校实验室的 A 同学刷了半个月算法题进考场发现大题全是 fork 返回值、三次握手、static 作用域出来直呼翻车。拿分的路径很清楚先把高频模块的答题模板背到能脱口而出再用真题把模板磨成自己的话。适合两类人——准备校招和社招笔试的以及工作几年但概念已经生锈、面试一追问就卡壳的。下面按考点模块拆每个模块都给到能直接背的答法和容易失分的地方。2. 进程与线程嵌入式 C 笔试里分值最重的八道题2.1 四句话把定义、区别、优缺点答满面试官问“进程和线程有什么区别”标准答法分四层定义、单位、开销、通信。第一层给定义——进程是运行中的程序实例有独立的内存空间和执行环境线程是进程内的执行单元同一进程的多个线程共享地址空间。第二层给出基本单位——进程是资源分配的基本单位线程是处理器调度的基本单位这句话几乎是必背得分点答不出来后面讲得再详细也难补救。第三层给开销——进程创建和切换要陷入内核、切换页表、刷新 TLB开销大线程切换只换栈和寄存器开销小。第四层给通信——进程间要借助内核提供的 IPC线程间直接读写共享变量但必须加同步。优缺点跟着这套逻辑走。多进程的优点是隔离性好一个进程崩了不影响别人缺点就是前面说的开销和资源占用多线程的优点是创建快、切换快、共享数据方便缺点是共享资源要同步容易出竞态和死锁一个线程踩坏内存整个进程都得陪葬。很多考生在这里只背“进程稳定、线程高效”面试官一追问“为什么稳定”就卡住其实答案就在地址空间是否独立这一句上。选型题一般会给场景答法要有判断依据。比如“一个服务要同时处理多路连接”答线程池加 IO 多路复用因为连接之间要共享会话状态“要跑不可信的第三方模块”答独立进程崩了主进程还能把它拉起来容错优先。再补一句“线程数量受栈空间和内核线程上限约束不是越多越好”分就齐了。状态转换考三态模型就绪、运行、阻塞。阻塞是主动的等 IO、等锁、等信号量、sleep 都会进阻塞就绪是被动的时间片用完、被更高优先级抢占都会从运行回到就绪此时资源已经齐了只差 CPU。这句话要能反过来答问“什么时候从阻塞变就绪”答“它等的事件发生了比如数据到了、锁释放了”。2.2 fork 与父子进程返回值那道题有固定答法fork 几乎是必考题因为它一箭三雕考系统调用、考返回值、考进程复制。答题顺序是先说作用再说返回值最后说复制行为。fork 的作用是创建一个新进程新进程是调用者的副本。返回值有三个分支父进程拿到子进程的 PID子进程拿到 0失败返回 -1 并设置 errno。为什么子进程拿到 0因为子进程不需要知道自己的 PID用 getpid() 就能拿而父进程要靠返回值区分自己和子进程所以给一个非零值。这个“为什么”经常被追问答得出来是明显加分项。复制行为要提写时复制Copy-On-Write。fork 出来的子进程在逻辑上拥有父进程地址空间的完整副本但内核并不立刻复制物理页而是把父子进程的页表都指向同一批只读页谁写谁触发缺页异常再复制。所以 fork 很快但如果子进程紧接着 exec 加载新程序这些复制基本是白做这也是 vfork、posix_spawn 存在的原因。#include stdio.h #include unistd.h #include sys/wait.h int main(void) { pid_t pid fork(); /* 调用一次返回两次 */ if (pid 0) { /* 失败父进程拿到负值 */ perror(fork); return 1; } else if (pid 0) { /* 子进程返回 0 */ printf(child pid%d, ppid%d\n, getpid(), getppid()); } else { /* 父进程返回子进程 PID */ printf(parent sees child pid%d\n, pid); wait(NULL); /* 回收子进程防止僵尸 */ } return 0; }这段代码要讲三点。pid 0 的分支必须写虽然实际很少进但面试官看的是你有没有错误处理意识。子进程分支用 getpid() 和 getppid() 打印自己的 PID 和父 PID父进程分支打印 fork 的返回值两个值对得上说明父子关系成立。最后的 wait(NULL) 是回收子进程不写的话子进程退出后留在僵尸态父进程一直不 wait 就会占着进程表项。参数上wait 的入参是 int* 用来接收退出状态传 NULL 表示不关心要判断子进程是正常退出还是被信号杀掉就用 waitpid 加 WIFEXITED、WEXITSTATUS 宏去解析。常考追问还有“子进程能不能继承父进程的文件描述符”答能fork 会把打开的 fd 表复制一份引用计数加一父子共享同一个文件偏移量这也是管道能工作的基础。还有一个经典陷阱题循环里 fork 三次问最后有几个进程。答 2 的 3 次方等于 8因为每次 fork 之后父子都继续往下跑画一棵二叉树就出来了。答题时把进程树画在草稿纸上比在脑子里数靠谱得多。2.3 通信与同步一张对比表选对 IPC 和锁通信这块最容易答成一锅粥因为进程通信和线程通信的名字有重叠。分开记进程之间内存不共享所以必须有内核参与线程之间内存共享所以重点是同步而不是传输。进程通信的六种常见方式——管道、命名管道、消息队列、共享内存、信号量、套接字。答题时按“有没有血缘要求、是否单向、数据要不要拷贝、能不能跨主机”四个维度去对比比背定义有用。方式血缘要求方向数据拷贝跨主机典型场景匿名管道必须有单向两次否shell 里的管道串联命名管道无单向两次否无亲缘进程间简单通知消息队列无双向两次否带边界的短消息共享内存无双向零次否大块数据高频交换信号量无不传数据无否只做计数与互斥套接字无双向两次是跨机通信、本机也可用表格里“数据拷贝”这一列是拉开差距的地方。共享内存最快因为数据从用户缓冲直接落到共享页内核不参与搬运代价是必须自己做同步不然读到一半被改写就是玄学问题。管道和消息队列都要经过内核缓冲写一次拷一次、读一次再拷一次所以有两次拷贝开销。套接字跨主机但本机用 Unix 域套接字比 TCP 回环还快因为它不走协议栈校验和。线程同步的四件套互斥锁、读写锁、条件变量、屏障。互斥锁是最基本的保护临界区读写锁适合读多写少多个读者可以并发写者独占适合配置表这类更新稀疏的数据条件变量不保护资源它解决的是“等某个条件成立”必须和互斥锁配合用判断条件要用 while 而不是 if防止虚假唤醒屏障是让一批线程在某个点对齐再一起往下走多阶段并行计算里常用。#include pthread.h #include stdio.h static int counter 0; /* 共享资源 */ static pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */ void *worker(void *arg) { for (int i 0; i 100000; i) { pthread_mutex_lock(lock); /* 进入临界区前加锁 */ counter; /* 只有这一行是临界区 */ pthread_mutex_unlock(lock); /* 用完立刻释放 */ } return NULL; }counter看着是一行编译出来是读、加、写三步两个线程交叉执行就会丢更新这就是竞态的典型例子。锁的粒度要尽量小临界区里不要放 printf、不要放阻塞调用否则锁持有时间被拉长吞吐直接掉下来。另外 PTHREAD_MUTEX_INITIALIZER 只适用于静态分配的锁动态分配的要用 pthread_mutex_init 初始化、pthread_mutex_destroy 销毁用错场景会当场翻车。2.4 死锁与状态转换把四个条件和四种处理讲成一条线死锁的答法分三段先说定义再说四个必要条件最后说处理策略。定义是两个或以上的进程互相持有对方需要的资源并且都不释放形成循环等待谁也无法推进。四个条件是互斥、占有并等待、不可抢占、循环等待缺一个就构不成死锁。这里要能举例单车道桥上两车对向而行就是互斥加占有并等待加不可抢占加循环等待的完整现场。处理策略要分清四种很多考生只答“避免死锁”一问细节就卡住。预防是破坏四个条件之一比如一次性申请全部资源破坏占有并等待按全局编号顺序申请破坏循环等待。避免是在运行时用银行家算法判断这次分配是否安全安全才给。检测是定期跑算法找环找到再恢复。恢复手段是杀进程、回滚检查点或者剥夺资源。实际工程里最常用的是“统一加锁顺序”和“加超时”因为银行家算法需要预知最大需求嵌入式场景基本用不上答出这句能显出你有实际判断。状态转换那题答三态加挂起态就够。新建到就绪是资源就绪就绪到运行是调度器选中运行到阻塞是主动等待事件阻塞到就绪是事件到来运行到就绪是时间片用完或被抢占。挂起态是把进程换出到磁盘分就绪挂起和阻塞挂起两种嵌入式里内存紧张时才会用。被问到“阻塞和挂起有什么区别”答阻塞还在内存里只是不参与调度挂起已经被换出内存。3. 网络编程TCP/UDP 与三次握手的稳定得分区3.1 从区别一路答到选型问 TCP 和 UDP 的区别最差的答法是“一个可靠一个不可靠”就停了。得按连接、可靠性、传输方式、开销、适用场景五个维度铺开。连接TCP 面向连接通信前必须三次握手建立连接结束后四次挥手释放UDP 无连接发出去就不管了。可靠性TCP 有确认应答、超时重传、序号重组、校验和能保证数据不丢、不重、不乱序UDP 只有校验和丢了就丢了。传输方式TCP 是字节流没有消息边界所以才有粘包问题UDP 是数据报一个包一个边界收的时候要么整包收到要么收不到。开销TCP 头 20 字节起步加上连接管理和拥塞控制延迟高UDP 头只有 8 字节没有流控和拥塞控制延迟低。选型题给场景就套文件传输、网页、邮件、需要顺序和完整性的选 TCP音视频流、实时游戏、DNS 查询、广播多播选 UDP。追问“UDP 能不能做可靠传输”答能在应用层自己实现确认、重传、序号和拥塞控制不少实时音视频协议就是这么干的代价是把复杂度从内核搬到了业务代码里开发成本和调试难度都上去了。再补一层“TCP 为什么可靠”四套机制要能报出来应答和超时重传保证不丢顺序号和重组保证不乱序滑动窗口和流量控制保证接收方不被冲垮校验和保证内容没被打坏。答完这四条再加一句“TCP 只保证传输层面可靠应用层语义还得自己做幂等”这是区分背书和理解的句子。3.2 三次握手与四次挥手序号和状态是得分点这题几乎每场必出答的时候要把报文里的标志位和序号变化说清楚只讲“客户端发 SYN、服务器回 SYNACK、客户端再回 ACK”只能拿一半分。三次握手的完整流程客户端发 SYN1、seqx进入 SYN_SENT服务器收到后回 SYN1、ACK1、seqy、ackx1进入 SYN_RCVD客户端再发 ACK1、seqx1、acky1双方进入 ESTABLISHED。为什么要三次而不是两次因为要让双方都确认对方的收发能力都正常两次的话服务器无法确认客户端能收到自己的包而且网络里历史重复的 SYN 会让服务器建立无效连接白白占资源。四次挥手主动关闭方发 FIN1、sequ进入 FIN_WAIT_1对方回 ACK1、acku1进入 CLOSE_WAIT主动方进入 FIN_WAIT_2对方把剩余数据发完后发 FIN1、seqw、acku1进入 LAST_ACK主动方回 ACK1、ackw1进入 TIME_WAIT等 2MSL 后关闭。为什么是四次因为 TCP 是全双工的一方说“我没数据要发了”不代表另一方也发完了ACK 和 FIN 不能合并硬合并会让被动方无法把剩余数据传完。TIME_WAIT 为什么要等 2MSL一是保证最后一个 ACK 能到达对方丢了对方会重发 FIN还有机会补二是让本次连接的残留报文在网络里消散避免污染下一个用相同四元组的连接。服务器上大量 TIME_WAIT 是常见追问答调小等待时间或开启连接复用参数也可以从架构上让主动关闭方落在客户端那一侧。状态名要能报全LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK、TIME_WAIT、CLOSED。面试官经常随机点一个问“这个状态下能收什么包”比如 CLOSE_WAIT 堆积说明应用层没调 close这是排查线上问题的常见入口能说出这句比背状态名更值钱。3.3 MTU 1500 与粘包三种解法的取舍MTU 是链路层能传输的最大数据帧长度以太网常见 1500 字节。这个数字要能解释来源以太网帧的数据字段最大就是 1500 字节所以链路层 MTU 定成 1500。IP 层分片、TCP 层 MSS 都跟它有关MSS 一般等于 MTU 减去 IP 头和 TCP 头IPv4 下就是 1500-20-201460。追问“MTU 能不能改”答能但改大了中间链路不支持会分片或者丢包实际调优一般是保持 1500 不动改 MSS 去规避隧道封装带来的额外开销。粘包不是 TCP 的 bug是字节流的必然结果。发送方连写两次接收方一次 recv 可能全收到也可能收到半个因为 TCP 只保证字节顺序不保证边界。UDP 没有这个问题因为它有消息边界。答题时要把“粘包”和“拆包”一起说因为实际场景里两个总是同时出现。三种解法要能说清取舍。定长包最简单每个包固定 N 字节不够补齐缺点是浪费带宽适合定长指令。分隔符在包尾加换行符或特定字节实现简单缺点是内容里不能出现分隔符出现了就得转义转义逻辑一多就容易出 bug。包头加包体最通用包头里放魔数、版本、长度、校验接收方先读固定长度的头解析出长度再读包体缺点是解析代码要处理半包状态得维护一个接收缓冲区。答题时补一句“长连接场景优先包头加包体短指令场景用分隔符定长包留给协议固定的老设备”比只背三种名字更像做过事。再追一句“缓冲区要设上限长度字段是外部输入直接拿去 malloc 就是内存攻击的入口”这是安全意识的加分点。3.4 OSI 七层背的顺序和每层的抓手七层从下到上物理、数据链路、网络、传输、会话、表示、应用。背的时候抓每层的一个关键词物理层管比特流和电气特性数据链路层管帧、MAC 地址和差错检测网络层管包、IP 地址和路由传输层管端到端TCP 和 UDP 在这一层会话层管会话的建立和拆除表示层管格式转换、加密、压缩应用层管 HTTP、FTP、SMTP 这些协议。追问一般是“某个设备在哪一层”中继器和集线器在物理层交换机和网桥在数据链路层路由器在网络层网关在传输层及以上。再追问“TCP/IP 四层模型怎么对应”答网络接口层对应物理加数据链路网络层对应网络传输层对应传输应用层把会话、表示、应用三层合并。这个对应关系要能双向答从七层说四层、从四层说七层都别卡。4. Linux 内核、内存与 ARM 外设容易被连环追问的一段4.1 内核组成与系统调用read/write 到底走了哪几步Linux 内核六大块进程管理、内存管理、文件系统、设备驱动、网络协议栈、系统调用接口。答题时每块补一句职责就够了比如进程管理负责创建、调度、销毁以及进程间通信内存管理负责物理页分配、虚拟地址映射和换页设备驱动把硬件操作封装成统一接口。用户空间和内核通信的方式系统调用、中断、文件操作设备文件、IPC 机制。系统调用是主动的中断是被动的文件操作是把设备抽象成文件后的统一入口这四种要能分开说别混成一锅。read/write 的内核动作是高频追问。以 read 为例用户程序把缓冲区和长度传进 syscallCPU 从用户态切到内核态内核根据 fd 找到对应的 file 结构再找到 inode 和具体的文件系统或驱动检查权限和参数合法性如果数据不在页缓存里就发起磁盘或设备读取把数据先读到内核缓冲最后把内核缓冲的数据拷贝到用户缓冲区返回实际读到的字节数。write 反过来先把用户数据拷进内核缓冲再交给文件系统或驱动落盘或发送。这段要强调两次拷贝和一次上下文切换这是理解零拷贝的前提。sendfile、mmap 这些优化就是为了省掉用户空间和内核空间之间的那次拷贝。追问“内核态和用户态的区别”答三点特权级别不同、能访问的资源不同、出错的影响范围不同。再追问“为什么要分”答隔离和保护用户程序写坏了最多自己崩不会把内核一起带走。4.2 堆与栈、内存泄漏与内存溢出堆和栈的对比要按分配方式、管理者、大小、生长方向、生命周期五个角度答。栈由编译器自动管理函数调用时分配、返回时释放遵循后进先出大小通常几 MB超了就栈溢出堆由程序员手动 malloc/free操作系统管理空闲链表大小受进程地址空间和物理内存限制容易产生碎片。生长方向上栈通常从高地址往低地址长堆从低地址往高地址长两者相向而行中间是共享库和映射区。内存泄漏和内存溢出经常被混。泄漏是该释放的没释放程序还活着但内存越吃越多长时间运行会耗尽内存溢出是写超了分配的边界把相邻数据踩坏轻则数据错乱重则崩溃栈溢出还会覆盖返回地址是安全漏洞的常见来源。答题时要给一个对比例子malloc 之后忘了 free 是泄漏strcpy 往 8 字节的数组里拷 20 字节是溢出。追问“怎么查泄漏”答三档代码审查看 malloc/free 是否配对用 valgrind 加泄漏检查参数跑一遍能定位到具体行嵌入式环境资源紧张就用 mtrace 或者自己封装 malloc/free 做计数。再补一个常见坑free 之后指针成了悬空指针再解引用是未定义行为规范做法是 free 之后立刻置空。大小端判断也在这块考。大端是高位字节放低地址小端是低位字节放低地址。判断方法有三种取地址比较字节、联合体、强制类型转换看第一个字节。第三种最常写#include stdio.h int main(void) { unsigned int x 0x12345678; unsigned char *p (unsigned char *)x; /* 低地址是 0x78 说明小端是 0x12 说明大端 */ printf(%s\n, (*p 0x78) ? little endian : big endian); return 0; }x取到的是四字节整数的首地址强转成 char* 之后解引用只读一个字节读到的正是低地址上那一个字节。0x12345678 的低位字节是 0x78所以读到 0x78 就是小端。这段代码的关键在于“低地址放的是哪个字节”答题时把这句说清楚比背结论重要。追问“什么时候用大端”答网络字节序和部分通信协议规定用大端跨平台传数据要先做字节序转换。4.3 Bootloader 与启动流程五步顺序不能乱嵌入式启动这条链路要能一口气说下来。上电后 CPU 从固定地址取第一条指令这段代码在片内 ROM 里做最基础的时钟和内存初始化然后把 Bootloader 从存储介质搬到 RAM 并跳过去。Bootloader 分两阶段第一阶段多用汇编做硬件初始化、关看门狗、设置栈、初始化 DRAM第二阶段用 C 写提供命令行、支持串口或网络下载、读内核镜像到内存、传启动参数。内核解压并初始化挂载根文件系统最后启动 init 进程。Bootloader、内核、根文件系统三者的关系Bootloader 是搬运工和交接人内核是管理者根文件系统是运行时的物质基础缺一个系统就起不来。常见追问是“内核起来后第一个进程是谁”答 init 或 systemdPID 为 1所有用户进程都是它的后代它挂了系统就 panic。启动参数要提一句Bootloader 通过命令行或设备树把内存大小、控制台串口、根文件系统位置传给内核参数错了典型现象是内核起来但挂载根文件系统失败串口打印一串 mount 错误。这种“现象对原因”的答法比只背流程更像真调过板子。4.4 ARM 存储器与五种外设接口对比存储器那块按易失性和可写性分。ROM 只读、掉电不丢放引导程序和固件RAM 可读写、掉电丢放运行时的程序和数据SDRAM 是同步动态 RAM靠时钟同步容量大成本低是主存的主力DDR 在时钟上下沿都传数据速率翻倍Flash 掉电不丢又可擦写放固件、文件系统和用户数据NOR 可以片上执行NAND 容量大但需要坏块管理和 ECC。五种接口要按线数、速率、拓扑、典型场景做对比。接口线数典型速率拓扑典型场景UART2TX/RX几十 bps 到几 Mbps点对点调试串口、GPS 模块I2C2SCL/SDA100k/400k/3.4M bps多从机、地址寻址传感器、EEPROMSPI4MOSI/MISO/SCK/CS几十 MHz一主多从、片选Flash、LCD、高速 ADCUSB4D/D-/VCC/GND480 Mbps 到数十 Gbps主机加设备外设、下载调试Ethernet4 对差分10M/100M/1G/10G星型局域网、远程通信选型题就看三个变量速率、距离、引脚预算。引脚紧张选 I2C要高速选 SPI跨设备选 USB跨机房选以太网点对点低速选 UART。I2C 的两个坑要能说总线要加上拉电阻多主机时要做仲裁SPI 的坑在片选多个从机要么用多根片选线要么用译码别把片选直接接地。5. 易错点六组一答就露怯的题5.1 概念答反类三组看着像其实完全不同进程通信和线程通信错在哪把互斥锁、信号量当成进程通信方式答上去或者把共享内存说成线程通信。为什么错进程之间地址空间独立共享内存必须由内核在物理内存里划一块再映射到两个进程本质上是 IPC线程本来就共享地址空间直接读写变量就行根本不需要“共享内存机制”。怎么答对先讲地址空间是否共享再讲传输介质最后讲同步手段三句话分开说别混在一句里。中断和异常错在哪把除零、缺页说成中断。为什么错中断来自 CPU 外部是异步的比如键盘、定时器、网卡异常是 CPU 执行指令时自己检测到的同步事件比如除零、非法指令、缺页、断点。怎么答对给一句“中断是外来的异常是执行指令时自己发现的”再各举两个例子。追问“缺页算不算中断”答算异常它是指令执行过程中因为地址映射缺失触发的处理完还能回到原指令重试。硬链接和软链接错在哪说硬链接是快捷方式。为什么错硬链接是同一个 inode 的多个目录项删除其中一个文件还在不能跨文件系统不能链目录软链接是独立 inode内容是目标路径原文件删了链接就失效可以跨文件系统可以链目录。怎么答对抓 inode 和跨文件系统两个点一句话说透“硬链接是同一份数据的多个名字软链接是指向名字的路径”。5.2 细节漏答类三个一追问就卡的地方三次握手只答三步不答为什么。错在哪把流程背完就停。为什么错面试官问的是理解不是记忆两次握手无法确认客户端的接收能力也无法防止历史重复 SYN 建立无效连接。怎么答对三步流程加一句“第三次 ACK 是为了让服务器确认客户端已收到 SYNACK”再补 TIME_WAIT 等 2MSL 的两个理由。static 只答“静态”。错在哪只说 static 修饰变量。为什么错static 有三个不同作用域修饰局部变量是延长生命周期到程序结束且只初始化一次修饰全局变量和函数是限制链接属性为文件内可见C 里修饰类成员是属于类而不是对象。怎么答对按三个场景分开说每个场景给一句效果再加一句“未初始化的静态变量默认清零普通局部变量不会”。new 和 malloc 混着答。错在哪只说 new 是 C 的、malloc 是 C 的。为什么错真正的区别在构造析构、返回类型、失败处理、是否可重载。new 会调用构造函数、返回具体类型指针、失败抛 bad_alloc、可以被重载malloc 只分配原始内存、返回 void*、失败返回 NULL、不可重载。怎么答对按这四个维度列表式回答最后补一句“new 的底层通常还是调 malloc 拿内存再在上面跑构造函数”。5.3 场景答偏类三个把送分题答成送命题的“什么时候用进程什么时候用线程”只答定义本文还有配套的精品资源点击获取