微博C++面经

1、HTTP的长连接和TCP长连接的区别?
概念上:
- TCP 长连接:指的是在 TCP 层面上建立一次连接(
connect→3-way handshake),然后保持该 TCP 连接不关闭,后续多次数据交换都复用同一条 TCP 连接,直到显式close。这是 transport 层的概念。 - HTTP 长连接(持久连接):是 HTTP(应用层)协议里的用法,指客户端与服务器在一次 TCP 连接上进行多个 HTTP 请求/响应,不在每个请求后关闭 TCP。实现上依赖于 TCP 长连接(HTTP 1.1 默认持久连接)。
关键区别 / 交互:
- HTTP 的“长连接”是依赖于底层的 TCP 长连接的——没有 TCP 长连接,HTTP 无法复用底层连接。
- 但不等同:TCP 长连接只保证底层通道持续存在;HTTP 长连接还涉及请求复用策略、并发请求管理、应用协议的头部压缩、流量控制等(尤其在 HTTP/2/3 中更复杂)。
实际关注点:
- 在 HTTP/1.0:默认短连接(每个请求后 close),HTTP/1.1:默认持久连接(
Connection: keep-alive),用同一 TCP 连接顺序发送多个请求(注意 head-of-line blocking)。 - 在 HTTP/2:在单个 TCP 连接上通过二进制帧把多个并发流(streams)复用,实现多路复用、优先级、头压缩;因此应用层可以在一条 TCP 连接内并发多个请求。
- 在 HTTP/3:HTTP 在 QUIC(UDP 上的传输)上运行,QUIC 本身实现了多路复用、连接迁移、零 RTT 等。
工程注意:
- TCP 长连接要考虑空闲超时、keepalive、连接重置与网络故障恢复、NAT超时。
- HTTP 层面要考虑并发请求控制(HTTP/1.x 的 pipelining 很少使用,HTTP/2/3 使用流),以及 header 压缩(避免重复)和 head-of-line blocking 的差异。
2、GET和POST的区别?
语义与用途:
- GET:用于从服务器获取资源(安全、幂等——按语义不应改变服务器状态);参数通过 URL(查询字符串)或路径传递。
- POST:用于提交数据以创建/修改资源(非幂等,可能会改变服务器状态);数据放在请求体(body)中。
技术差别:
- URL 长度:GET 的参数放在 URL,受浏览器/服务器/代理对 URL 长度限制(实际上也有,但通常限制在几 KB);POST 可以承载更大 body(受服务器配置限制)。
- 缓存:GET 可被缓存(Cache-Control、ETag 等);POST 默认通常不被缓存(除非显式指示)。
- 可书签/可分享:GET 请求生成的 URL 可被收藏/分享;POST 不行(因为数据在 body)。
- 请求幂等性:理论上 GET 幂等(多次相同请求不应改变服务器状态),POST 非幂等(多次提交可能产生不同效果)。
- 可见性:GET 参数出现在 URL,敏感数据不应放在 URL(日志、浏览器历史、Referer 都会泄露);POST 的 body 在网络中仍然可见(若无 TLS 则被窃听),但不会出现在 URL。
实现:
- Web 表单默认 method=GET 或 POST。很多 REST 设计把安全的读取用 GET,创建资源用 POST,更新用 PUT/PATCH,删除用 DELETE。
- 服务端判断请求体解析与 Content-Type(
application/x-www-form-urlencoded,multipart/form-data,application/json等)。
3、了解的锁、ABA 问题;如果两个线程一直自增一个值、不加锁,最后值会是多少?有没有范围?
3.1、常见锁的种类与适用场景
- 互斥锁(mutex / std::mutex):互斥访问资源;适合临界区不大、开销可接受的场景。
- 递归互斥锁(recursive_mutex):允许同一线程重复加锁(避免死锁),但通常尽量避免使用。
- 读写锁(rwlock / shared_mutex):多读单写优化,读并发高、写少的场景适用。
- 自旋锁(spinlock):短临界区、并发线程在同一 CPU 上短时间等待时优;内核态/低延迟场景常用。
- 自适应锁、ticket lock、MCS/CLH 分布式锁:用于减少公平性问题、降低 cacheline 争用,适合高争用场景。
- 信号量(semaphore):计数同步,控制资源数量(例如可用连接池大小)。
- futex(fast userspace mutex):Linux 上用户态->内核态少的轻量互斥实现(用户态自旋,必要时陷入内核)。
- 乐观并发控制 / 无锁原语:
- CAS(compare-and-swap)/ atomic:实现无锁队列、无锁栈等。
- seqlock:写者写序号+数据,读者重试;适合写少读多且允许读重试的场景。
- RCU(read-copy-update):读者无锁,写者复制并更新,适合高并发读、低频写、大对象生命周期延迟回收。
- 事务内存(HTM):硬件事务内存,短期替代锁的想法,但依赖硬件支持并非通用。
3.2、ABA 问题
什么是 ABA 问题: 在基于 CAS 的并发设计中,线程 T1 读取某个值是 A,然后被抢占;其它线程把值从 A 改为 B,又改回 A;T1 继续使用 CAS 比较当前值是否仍为 A,于是 CAS 成功,但实际上中间值已经被修改过(A→B→A),导致 T1 以为数据没改变,可能出现逻辑错误(比如指针所指对象已被释放并重用)。
示例(无回收保障的链表 pop):
T1: old = head; // old == A (ptr)
T1: next = old->next;
... 被抢占 ...
T2: pop old (A), free old, alloc new1 at same地址 (A), push new1 -> head points to A again
T1: CAS(&head, old, next) // 成功(因为 head == A),但 old 实际已被替换 -> 错误危害: 会导致内存安全问题(悬挂指针、双重释放、数据错乱)。
典型解决办法:
- 带版本号的指针:把版本号与指针一起 CAS(双字 CAS 或
atomic<uint64_t>把低位当指针,高位当计数器),每次修改把计数器加一。即便指针值相同,版本号不同 CAS 失败。 - 双宽 CAS(DCAS)或 CAS on (ptr, tag):在支持的硬件/库上使用。
- 内存回收策略:
- Hazard pointers:在使用对象前把指针标记为“hazard”,回收线程检查 hazard 指针再释放,避免被重用。
- Epoch / RCU:延迟回收,在没有并发读者的 epoch 后才释放。
- 引用计数(shared_ptr):自动防止对象提前释放(但有性能代价与循环引用问题)。
- 使用更高层次并发容器:例如 libcds、concurrent_hash_map,避免自行处理 ABA。
3.3、两个线程不停地自增同一个值、不加锁,最后值会是多少?有范围吗?
情形明确化:设初始值 v0,线程 A 执行 N_A 次 x = x + 1(非原子读-改-写),线程 B 执行 N_B 次 x = x + 1,无任何同步,操作顺序由调度决定。
结论(重要且要精确):
理想情况下(无竞争、全部成功序列化)最终值应是
v0 + N_A + N_B。实际无锁竞态下,因为读/写操作互相覆盖,会出现丢失更新或覆盖回退,最后的结果不可预测,会在某个可行区间内,但并非严格单调增加。
可以给出一个包含关系的保守范围:最终值一定等于某次读出的值 + 1(最后一次写入的值)。因此最小可能值是
v0 + 1(最极端情况:最后一次写基于原始v0),最大可能值是v0 + N_A + N_B(如果所有增量都成功串行化)。因此:final ∈ [ v0 + 1, v0 + N_A + N_B ]说明:
v0本身不会减少(写永远是读值 + 1);但因为写可能基于很久之前读到的旧值,存在“回退”现象(例如某次写覆盖了比当前更大的值,导致值变小),因此没有单调性保证。
示例说明(丢失更新):
// 伪代码 (非原子)
int x = 0;
// T1: read x -> r1, r1+1 -> 1, write 1
// T2: read x -> r2 (可能同时为 0), r2+1 -> 1, write 1
// 两次自增,最终可能为 1 而不是 2工程建议:
- 若多个线程并发修改共享计数:使用原子操作(
std::atomic<int>.fetch_add(1))或加锁保护;不要依赖编译器/CPU 的不可见顺序。 - 如果想要无锁且正确,使用硬件原语(
atomic、fetch_add、CAS)或设计无锁数据结构并配合内存回收策略。
4、红黑树与哈希表在应用场景中的选择
- 哈希表(unordered_map)
- 平均时间复杂度:
O(1)查找/插入/删除(均摊),最坏情况O(n)(大量冲突/退化)。 - 不保持元素顺序(无序)。
- 支持快速精确匹配(key→value)。
- 对迭代顺序没有排序保证;哈希表的内存开销通常较高(桶 + 链表/开放地址)。
- 平均时间复杂度:
- 红黑树(std::map)
- 平均与最坏均为
O(log n)查找/插入/删除。 - 保持键的有序性(中序遍历得到有序序列)。
- 支持范围查询(lower_bound / upper_bound)、有序迭代、按顺序查询前驱/后继。
- 内存开销通常更小(每个节点开销较大但没有桶稀疏效应),插入时保持平衡。
- 平均与最坏均为
如何选择?
需要有序数据或范围查询(range queries) → 使用红黑树(std::map / std::set / tree-based)。例如实现区间查找、前驱/后驱查找、按 key 排序输出。
只需要按键快速查找、插入、删除且不关心顺序 → 哈希表(std::unordered_map)。例如高速缓存、计数、集合 membership 测试。
内存和性能 trade-off:
- 小数据量:差别不大。
- 大量数据且对查找性能敏感:哈希表常更快;但如果 key 分布产生很多冲突或 hash 计算昂贵,性能下降。
并发场景:
哈希表容易做分段锁/分片(buckets 分片)以提高并发度;tree 在并发更新上更难做高并发高效实现(但也有并发树算法)。
缓存局部性:开放地址哈希(linear/quadratic probing)通常有更好缓存局部性,比指针链表的哈希更好。RB-tree 节点分散在堆上,迭代会跳转,缓存差。
实时/最坏时间需求:若对最坏情况有约束(例如不能接受最坏情况退化),红黑树给你
O(log n)的稳定上界;哈希表最坏为O(n)(但通过良好哈希和 resize 可缓解)。
工程示例:
- 实现 LRU 缓存(需要按访问顺序) → 用 hash + 双向链表。
- 统计词频(只需要查找/计数) → unordered_map。
- 有序索引/区间统计(例如数据库索引、区间树) → 树结构(RB/B-tree)。
5、HTTP/2.0 与 HTTP/3.0 的区别
底层传输:
- HTTP/2:运行在 TCP 之上(TLS 通常通过
ALPN协商h2)。 - HTTP/3:运行在 QUIC(基于 UDP 实现的用户态传输协议)之上,QUIC 集成了 TLS 1.3。
多路复用与 HOL(head-of-line)问题:
- HTTP/2:在一条 TCP 连接上多路复用多个 stream(通过二进制帧),但 TCP 在传输层有 head-of-line blocking:如果某个 TCP 包丢失,整个 TCP 流(即所有 HTTP/2 stream)会因为重传而停顿,导致所有并发 stream 都受影响。
- HTTP/3:QUIC 在传输层实现了基于 stream 的独立重传与流控制,单个丢包只影响对应的 QUIC stream,不会阻塞其他 stream,从而显著减轻 HOL 问题。
连接建立与 TLS:
- HTTP/2:依赖 TCP 三次握手 + TLS 握手(若使用 TLS),握手延迟较高。
- HTTP/3(QUIC):集成 TLS 1.3 到 QUIC 协议,支持 0-RTT / 1-RTT 连接建立(对重连友好且时延更低)。
部署与实现:
- HTTP/2:服务器与中间件(代理、负载均衡器)广泛支持,基于 TCP,容易被现有网络设备处理(但中间件需要支持 HTTP/2 帧)。
- HTTP/3:基于 UDP,网络中间件对 UDP 转发、负载均衡、ACL、DPI 行为可能不同,需要额外支持。QUIC 把很多运输逻辑放到用户态实现(更灵活但更复杂)。
头压缩:
- HTTP/2:HPACK(针对压缩表的双端状态同步)会在某些攻击/并发情形下有复杂性。
- HTTP/3:使用 QPACK,为多路复用而设计,减少队头阻塞问题。
优劣总结:
- HTTP/3 优点:更低连接延迟、抗丢包能力更好、连接迁移(IP 变更或移动网络)更友好。
- HTTP/2 优点:部署成熟、与现有 TCP 基础兼容性好。
- 工程注意:HTTP/3 需要对 UDP 的中间设备兼容性进行评估(防火墙、流量监控),但在移动场景、跨网络切换、丢包环境下能显著提升体验。
6、GCC 编译时常用选项 & 如何生成符号表
常用 GCC 编译选项(按用途分类)
语言标准与预处理
-std=c++17、-std=gnu++11:指定语言标准-E:仅预处理-c:编译但不链接(生成.o)-S:生成汇编
优化
-O0:无优化(易于调试)-O1,-O2,-O3:逐级优化-Os:优化代码大小-Ofast:激进优化(可能违反标准)-Og:为调试而优化(推荐开发时使用)
调试/符号
-g:生成调试信息(DWARF)-g3:包含宏信息等-ggdb:为 GDB 生成更详细信息
警告/错误
-Wall:开启常用警告-Wextra:更多警告-Werror:把警告当错误处理
代码生成 / 链接
-fPIC:生成位置无关代码(用于共享库)-shared:生成共享库.so-static:静态链接(不使用动态库)-march=、-mtune=:指定指令集/性能调优-flto:链接时优化(LTO)
安全
-fstack-protector-strong、-fstack-protector-all:栈溢出保护-pie、-fPIE:生成可重定位可执行(ASLR 友好)
链接器传递
-Wl,-rpath,/some/path:传参数给链接器-Wl,-z,defs:链接检查
如何生成符号表 / 调试符号
- 编译时:使用
-g(例如g++ -g -O0 main.cpp -o main)会把调试信息写入可执行文件(DWARF)。 - 查看符号:
nm -C a.out:列出符号表(C++ 符号反混淆用-C)。readelf -s a.out:查看符号表和符号信息。objdump -t a.out:列出符号与节信息。
- 动态符号导出(用于运行时栈回溯):
- 链接时加
-rdynamic(把符号放入动态符号表,使得backtrace能解析符号名)。
- 链接时加
- strip:
strip a.out会移除符号表,减小体积;调试时不要 strip。 - 在共享库中保留调试:可以把 debug 信息放入外部文件(
.debug)来减少运行时文件大小(objcopy --only-keep-debug等)。
7、GCC 优化到变量 “不存在” 怎么办(调试时看到 “optimized out”)
发生原因:
编译器在 -O 优化等级下会把“不必要”或者无地址的变量优化掉(比如把变量放在寄存器、常量传播、死代码消除),导致调试器无法找到其内存地址,显示 optimized out。
解决方法 / 建议:
编译时降低优化等级:用
-O0 -g来确保变量存在并带有调试信息(最直接、最常用)。开发调试:
g++ -O0 -g ...使用
-Og:这是为调试而设计的优化等级,保留大部分调试信息同时做轻量优化。禁止特定优化:用
-fno-omit-frame-pointer、-fno-strict-aliasing或其他-fno-...选项(视问题而定)。标记变量为
volatile(不推荐仅为调试):volatile告诉编译器不要对该变量做某些优化,但会影响优化效果和生成代码,通常不建议只是为了解决调试问题而修改生产代码。在代码中打印 / 日志变量:如果无法改变构建,使用日志语句在关键点输出变量值。
优化与调试的权衡:在需要剖析优化影响时,编译两份二进制(调试版
-O0和 生产版-O2),或使用带符号的优化二进制(-g -O2),但注意 debug 信息的准确性可能受优化影响。
8、动态链接库与静态链接库的区别
静态链接库(.a)
- 在链接时把库的目标代码直接拷贝进最终可执行文件。
- 优点:
- 可执行文件独立,部署简单(不需依赖运行环境的库)。
- 启动时不依赖动态链接器,减小运行时故障风险。
- 缺点:
- 可执行文件体积大(包含库代码副本)。
- 如果修复库 bug 或更新库,需要重新链接/重新发布所有程序。
- 无法共享内存页(每个进程有独立副本)。
动态链接库(.so / .dll)
- 程序在运行时由动态链接器把库加载进进程地址空间;多个进程可共享同一库代码的物理页。
- 优点:
- 可执行文件体积小(只保存符号和依赖信息)。
- 库更新/补丁可在不重建可执行文件的情况下生效(如果 ABI 兼容)。
- 多个进程共享内存,节省内存。
- 缺点:
- 运行时依赖库版本(ABI 兼容性问题,所谓“依赖地狱”)。
- 动态加载时需要寻找并加载库(启动稍慢),并有动态重定位开销。
- 需要注意安全补丁与版本管理(例如 LGPL/licensing 约束)。
工程考虑:
- 对于系统库和频繁更新的第三方库通常选动态库;对嵌入式或需要单一二进制部署则倾向静态链接。
- 对安全与最小化攻击面时,有时选择静态(减少外部依赖),但会丢失自动更新修复的优势。
9、C++ 的四个类型强转
static_cast<T>(expr)- 编译期转换,适用于:数值类型转换、指针/引用的上行转换(Derived* → Base*)以及某些需要的转换。
- 不做运行时类型检查(向下转换
Base*→Derived*若不正确,会产生未定义行为)。 - 示例:
double d = 3.14; int i = static_cast<int>(d);
dynamic_cast<T>(expr)- 仅用于多态类型(类含有虚函数)。用于安全的运行时向下转换(Base* → Derived*),失败返回
nullptr(指针形式)或抛std::bad_cast(引用形式)。 - 示例:
Derived* pd = dynamic_cast<Derived*>(pb); if (pd) { /* 成功 */ }
- 仅用于多态类型(类含有虚函数)。用于安全的运行时向下转换(Base* → Derived*),失败返回
const_cast<T>(expr)- 修改对象的
const/volatile属性(去除或添加)。是唯一用于改变 cv-qualifier 的 cast。 - 仅当原始对象本身非
const时去除const才是安全的,否则对修改会导致未定义行为。 - 示例:
char* p = const_cast<char*>(some_const_char_ptr);
- 修改对象的
reinterpret_cast<T>(expr)- 位级重解释转换:把指针当作整数或把整数当作指针、不同指针类型之间的转换,语义上非常危险且高度平台相关。
- 示例:
uintptr_t v = reinterpret_cast<uintptr_t>(ptr); MyType* p2 = reinterpret_cast<MyType*>(v);
补充:尽量避免 C 样式 (T)x,因为它掩盖意图并且可能执行上述多个转换中的任意组合。使用 C++ 风格 cast 能提高可读性与安全性。
10、C++ 的 static 的作用
局部静态变量(函数体内
static):生命周期:静态存储期(程序运行直到结束),但作用域仅限于函数体(局部可见)。
初始化:C++11 起局部静态变量的初始化是线程安全的(按标准保证只初始化一次)。
用途:缓存、延迟初始化单例等。
示例:
cppint counter() { static int c = 0; return ++c; }
文件作用域的
static(旧式内部链接):在翻译单元级别(.cpp 文件)声明
static变量或函数会给符号内部链接(只能被本翻译单元访问)。推荐现代替代:匿名
namespace { ... }。示例:
cppstatic int helper_var = 0; // 仅本 .cpp 可见
类的静态成员变量:
属于类,而不是类实例;所有对象共享一份。需要在类外定义(除非
inline staticin C++17)。示例:
cppstruct S { static int count; }; int S::count = 0;
静态成员函数:
没有
this指针,不能访问非静态成员,只能访问静态成员。可以通过S::f()或对象调用。用途:工厂方法、工具函数。
示例:
cppstruct S { static void hello() { /* no this */ } };
静态在命名空间/全局变量中的作用
用于限制链接可见性(internal linkage)。现代代码用匿名 namespace 替代以更清晰。
C++17
inline static变量:允许在头文件中定义
inline static成员变量而不需要在 .cpp 中单独定义。
11、Linux 内核如何解决内存碎片?伙伴算法(buddy)说明
内核内存碎片的问题
- 内存碎片分为内部碎片(分配单元内部未使用)和外部碎片(可用碎片分散导致无法满足较大连续分配)。
- 操作系统需要同时满足小对象分配(比如缓存、kmalloc)和大页分配(如分配连续物理页、HugePages)。
伙伴算法(Buddy Allocator)— 核心思想
- 内核的物理页面按 2 的幂(order) 管理:
order 0= 1 个物理页,order 1= 2 页,order k=2^k页块。 - 有一个空闲链表数组
free_area[order],每一项链表保存该order大小的空闲块。 - 分配时:找到满足请求的最小
order,如果当前order没有空闲块,则从更大order'找一块然后一分为二(split)直到得到所需order。 - 释放时:释放的块会与它的“伙伴”(buddy)检查是否也空闲;如果空闲则合并(coalesce)成更高
order的块,并递归尝试合并,直到无法合并为止。
优点:
- 分配和合并速度快(常数时间,链表操作)。
- 简单,便于实现和分析。
缺点:
- 会产生内存碎片,尤其对于频繁的小冷启动/大块需求混合的场景(因为块大小是 2 的幂,导致内部浪费)。
- 不能很好地处理非 2 的幂尺寸的最优打包。
内核中的其它机制协同工作
- slab/slub/slsa 分配器:对小对象使用 slab/SLUB 分配器,预分配缓存对象,减少碎片并提高缓存命中。slab 提供对象缓存和对象构造/析构回调。
- 页迁移 / 内存压缩(compaction):当需要分配大的连续内存(大 order),内核可以触发内存压缩(compaction),把分散的页迁移到一起以形成连续空闲块,从而降低外部碎片影响。compaction 会移动页(需要考虑页映射、锁、写时复制)。
- 分区(zones):内核把物理内存分为
ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM等不同区域,按用途/硬件限制分配,防止跨区碎片影响。 - GFP 标志:分配 API 提供多种
GFP_*标志(例如GFP_ATOMIC,GFP_KERNEL),指示是否可睡眠、是否可触发回收/压缩等。 - HugePages 与 Transparent HugePages:为需要大连续页的应用(如数据库)提供大页策略,减少 TLB 压力,但也带来大页碎片风险。
总结:伙伴算法负责页级别的管理,slab/sllub 负责对象级的高效分配,内存压缩与迁移用于在需要大连续内存时减少碎片。工程上要根据分配模式选择合适分配器 API 与 GFP 标志,并尽量避免频繁分配大量不同大小的内存导致碎片。
12、进程 vs 线程 vs 协程;Linux 内核调度的是哪个?
进程 vs 线程
- 进程(process):
- 有独立的地址空间(虚拟内存)、文件描述符表等资源。
- 进程间通信(IPC)成本相对较高(管道、socket、shm 等)。
- 线程(thread):
- 轻量级:同一进程下的多个线程共享地址空间、文件描述符、信号处理等资源,但每个线程有自己的栈、寄存器上下文和线程局部存储(TLS)。
- 线程间直接访问共享内存,通信开销低,但需要同步(互斥)来避免竞态。
- 示例:POSIX 下
fork()创建进程,pthread_create()创建线程(线程属于进程)。
协程(coroutine)
- 语义:用户态的轻量协作式/非抢占式并发单元。协程在用户空间切换(无需内核调度或上下文切换到内核),通常通过显式
yield/await挂起和恢复。 - 特点:
- 切换成本非常低(仅用户态寄存器/栈切换)。
- 不依赖于内核的线程调度,适合大量并发但单核/多核情况下需配合线程池来并行运行协程。
- C++20 支持语言级协程(
co_await/co_yield)。
- 适用场景:IO 密集型、海量并发但单个任务消耗少的场景(server 的高并发连接处理)。
Linux 内核调度的是什么?
- 内核调度调度的是“任务(task)”,在 Linux 中,线程和进程都是
task_struct的实例(内核把线程视为轻量级进程)。因此内核调度的是线程/进程(task),不是协程(协程是用户态概念,内核不会直接感知,除非实现成内核线程)。 - 调度器类型:
- CFS(Completely Fair Scheduler):Linux 的主流时间分享调度器,用
vruntime来衡量每个任务的虚拟运行时间,尽量公平分配 CPU。 - 实时调度策略:
SCHED_FIFO、SCHED_RR等用于实时任务。 - 调度数据结构:每个 CPU 有自己的 runqueue,内核做负载均衡、迁移任务到其他 CPU 等。
- CFS(Completely Fair Scheduler):Linux 的主流时间分享调度器,用
总结:
- 内核负责抢占式调度运行在 kernel-space 的任务(线程/进程)。
- 协程是用户态的轻量任务,需要用户程序/运行时在线程中调度多个协程,常见模式是“若干线程 + 每线程调度大量协程”。
13、库函数和系统调用的区别
本质区别:
- 系统调用:执行特权操作,需要从用户态切换到内核态(陷入内核)。是操作系统提供的接口(例如
read,write,open,mmap,fork等)。系统调用成本高(上下文切换、参数检查、安全检查)。 - 库函数:在用户态运行的函数;它可能会直接调用系统调用,也可能只是纯用户态代码(例如
printf格式化字符串)。库函数是对系统调用的封装或纯用户态实现。
示例对比:
printf():属于 C 标准库函数,会做缓冲、格式化,最终在缓冲区满或\n/fflush时调用write(2)(系统调用) to flush。fopen():库函数,最终会调用open(2)(系统调用)进行文件打开,并在用户态管理FILE*缓冲结构。gettimeofday():传统上是系统调用,但现代 Linux 通过 VDSO(virtual dynamic shared object) 把某些时钟获取函数实现为用户态快路径以避免系统调用开销。
重要区别点:
- 开销:库函数通常开销小(如果仅用户态操作),系统调用开销大(内核切换)。
- 权限与能力:只有系统调用能执行内核提供的受保护操作(如 IO、fork、mmap、进程控制等)。
- 接口层次:库函数提供更高层次、可移植的接口,屏蔽底层系统调用差异。
14、Linux 中断:硬中断(硬 IRQ)与软中断(softirq)及为什么硬中断后还要加软中断?
中断机制分层(Linux 的 Bottom-half 模型):
- 硬中断:
- 由硬件触发,内核在中断上下文立即响应。
- 在硬中断处理时,通常中断被屏蔽/禁止(具体实现与并发有关),因此应尽量短小快速,完成最必要的工作(ack 硬件、中断计数)。
- 不能睡眠(不能执行会阻塞的操作)。
- 软中断:
- 用于把复杂或耗时的处理中后置(deferred work),在较安全的上下文执行。
- softirq:一种底层异步机制,在软中断上下文运行(仍然不能睡眠),内核会在合适时机(中断返回、抢占点、
ksoftirqd线程)执行 softirq。 - tasklet:基于 softirq 的较高层抽象(运行在 softirq 上,以单一 CPU 串行化)。
- workqueue:可以在进程上下文中执行(因此能睡眠),适合更耗时的任务。
为什么硬中断后要加软中断(为什么要分两段处理)?
- 硬中断必须短小:硬中断上下文对系统影响大(禁中断或降低中断响应),长时间运行会影响系统响应与中断延迟。
- 软中断可以延后处理耗时工作:例如网络包接收需要将数据复制给 skb、调用协议栈处理、唤醒进程等操作,这些可以放在 softirq 或 workqueue 中执行,从而把硬中断处理提升为“快速接收并排队+延后处理”。
- 负载调控:如果 softirq 工作很多,内核会把这些工作迁移到
ksoftirqd(一个普通内核线程)执行,以避免在中断返回时占用过长时间。 - 示例:网卡收到数据触发硬中断,硬中断只是把数据 DMA 到内存并把 skb 入 rx-queue,然后触发 NET_RX_SOFTIRQ;NET_RX_SOFTIRQ 在软中断上下文处理协议栈或调度到
ksoftirqd。
总结:硬中断用于快速响应硬件,软中断/底部半部分用于延后执行耗时或可并发的处理,以保证系统可响应性与可伸缩性。
15、TCP 半连接队列和全连接队列区别
背景(TCP 三次握手):当服务器监听端口 listen() 后,会有大量客户端发起 SYN,TCP 实现需要跟踪这些尚未完成三次握手的半连接以及已完成握手等待应用 accept() 的已连接。
两个队列的定义:
- 半连接队列(SYN queue):保存那些已收到客户端
SYN并已发送SYN-ACK,但尚未完成第三步ACK的连接(处于半开状态)。这些连接需要维护一些状态(如初始序号、超时重传计数)。目的是防止被大量未完成连接占满。 - 全连接队列(accept queue):保存那些三次握手已完成、处于 ESTABLISHED 状态但尚未被应用通过
accept()取走的连接。应用accept()时从此队列取出 socket。
工作流程:
SYN到达,内核在半连接队列中创建一个 pending entry,并发送SYN-ACK。- 当收到客户端
ACK,内核把该连接从半连接队列移到全连接队列(已完成),等待应用accept()。 - 应用
accept()会消耗全连接队列中的已完成连接。
SYN flood / 防护机制:
- SYN flood:攻击者发送大量
SYN而不完成握手,造成半连接队列耗尽,从而阻止合法连接。防护手段:- SYN cookies:在资源不足时不在内核分配半连接结构,而在
SYN-ACK的 ISN 中编码必要信息,只有在收到合法ACK时根据 cookie 重构连接,从而抵抗队列耗尽(代价是不能存储某些选项)。 - 调节 backlog 参数:
listen(fd, backlog),以及系统全局参数net.core.somaxconn、tcp_max_syn_backlog等(各内核版本语义有所差别)。 - iptables / rate limiting / SYN proxy 等防护。
- SYN cookies:在资源不足时不在内核分配半连接结构,而在
Linux 的实现注意点:
- Linux 在不同内核版本中对 backlog 的语义有细微变化(早期只有一个队列,后继实现区分 incomplete/complete 队列)。现代 Linux 有
tcp_max_syn_backlog、somaxconn、tcp_abort_on_overflow等调优项。 accept()队列满时,内核可能丢弃新到的完成连接或拒绝接收,表现为客户端连接失败。
工程实践建议:
- 对于高并发服务器:合理调大
listenbacklog 与系统内核参数,开启 SYN cookie(仅在必要时),并在应用层尽快accept()来缩短在全连接队列的停留时间(例如用线程池/IO 模型高效消费连接)。 - 使用
epoll/accept4/预先分配连接处理资源可降低队列增长。
