穩定性 > 效能 > 功能。長時間運行(月/年級)、高併發(萬級 QPS)、海量數據(GB/TB 級內存)是常態。本文基於 Linux 32/64 位、C/C++11/14/17 環境,總結十年踩坑與重構經驗。
// 經典分級內存池(參考 Nginx/Redis/LevelDB)
class MemoryPool {
// 大塊內存塊
struct Block { Block* next; char data[]; };
// 小塊鏈表(按 2^n 對齊:8, 16, 32, ..., 4096)
struct FreeList { void* head; size_t block_size; };
// 分配
void* alloc(size_t size) {
if (size <= MAX_SMALL) return alloc_small(size);
return alloc_large(size); // 直接 mmap/malloc
}
// 釋放(不歸還 OS,掛回自由鏈表)
void dealloc(void* ptr, size_t size) {
if (size <= MAX_SMALL) free_small(ptr, size);
else free_large(ptr);
}
// 批量銷毀(進程結束/Worker 回收)
~MemoryPool() { release_all(); }
};
關鍵點:
munmap,避免池污染。atexit)。// 基於工作盜演算法的線程池(參考 Java ForkJoinPool / Intel TBB)
class ThreadPool {
struct Task { std::function fn; };
// 每線程雙端隊列
std::vector> local_queues;
// 全域隊列
std::queue global_queue;
std::mutex global_mutex;
std::condition_variable cv;
// 提交任務
template
void submit(F&& f) {
size_t idx = thread_local_index();
if (idx != INVALID) {
local_queues[idx].push_back({std::forward(f)});
} else {
std::lock_guard lk(global_mutex);
global_queue.push({std::forward(f)});
}
cv.notify_one();
}
// Worker 循環
void worker_loop(size_t idx) {
while (running) {
Task task = pop_task(idx); // 先嘗試本地、再偷別人、最後搶全域
if (task.fn) task.fn();
else { std::unique_lock lk(global_mutex); cv.wait(lk); }
}
}
};
優勢:工作盜演算法實現負載均衡、無鎖本地隊列極低爭用、任務親和性利用 CPU 緩存局部性。
Linux 核心經典技巧,以零開銷實現結構體嵌入多態:
// 定義在
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
// 使用範例:通用鏈表節點嵌入業務結構
struct list_node { struct list_node *next, *prev; };
struct user_session {
int fd;
uint64_t session_id;
struct list_node node; // 嵌入而非指向
char buffer[4096];
};
// 從 list_node 反推 user_session
struct user_session *sess = container_of(node_ptr, struct user_session, node);
// 等價於 C++: sess = static_cast(node_ptr) // 前提是 node 是第一個成員
應用場景:通用容器(鏈表、哈希表、紅黑樹)、事件循環、狀態機、協議解析器——容器零業務依賴,業務零容器侵入。
多線程環境下,對象何時銷毀是核心難題。模仿 COM/內核 kref:
struct refcounted_obj {
std::atomic refcnt; // C11: _Atomic(int)
void (*destroy)(struct refcounted_obj*);
};
static inline void ref_get(struct refcounted_obj* obj) {
atomic_fetch_add_explicit(&obj->refcnt, 1, memory_order_relaxed);
}
static inline void ref_put(struct refcounted_obj* obj) {
if (atomic_fetch_sub_explicit(&obj->refcnt, 1, memory_order_acq_rel) == 1) {
// 最後一個引用,執行銷毀
atomic_thread_fence(memory_order_acquire);
obj->destroy(obj);
}
}
// 使用:容器持有引用、回調持有引用、異步任務持有引用
// 只有所有持有者 release,對象才真正銷毀
記憶體序選擇:relaxed 計數增減、acq_rel 最後釋放配合 acquire fence 確保銷毀前寫入可見。
網路協議常見 [長度][類型][負載...],C 結構體直接映射變長數組:
// 錯誤:指針會佔用 8 字節,破壞佈局
struct BadMsg { uint32_t len; uint8_t type; char *payload; };
// 正確:柔性數組成員(C99)或單元素數組(C89 相容)
struct GoodMsg {
uint32_t len;
uint8_t type;
uint16_t flags;
char payload[1]; // 或 char payload[]; (C99)
};
// 分配
struct GoodMsg* msg = malloc(sizeof(struct GoodMsg) + payload_len - 1);
msg->len = htonl(payload_len + HEADER_SIZE);
msg->type = MSG_TYPE_DATA;
memcpy(msg->payload, data, payload_len);
// 發送
write(fd, msg, ntohl(msg->len));
對齊陷阱:#pragma pack(1) 或 __attribute__((packed)) 強制 1 字節對齊,但可能導致非對齊訪問效能下降(ARM 嚴重)。建議:手動填充對齊,保持自然對齊,網路序統一用 htonl/ntohl。
// Reactor 模式核心循環
while (running) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; ++i) {
Connection* c = (Connection*)events[i].data.ptr;
if (events[i].events & EPOLLIN) handle_read(c);
if (events[i].events & EPOLLOUT) handle_write(c);
if (events[i].events & (EPOLLERR|EPOLLHUP)) handle_close(c);
}
}
// 連接狀態機
enum ConnState { READ_HEAD, READ_BODY, PROCESS, WRITE_HEAD, WRITE_BODY, CLOSED };
void handle_read(Connection* c) {
while (true) {
ssize_t n = read(c->fd, c->read_buf + c->read_pos, c->read_buf_size - c->read_pos);
if (n > 0) {
c->read_pos += n;
if (c->state == READ_HEAD && c->read_pos >= HEADER_SIZE) {
parse_header(c);
c->state = READ_BODY;
}
if (c->state == READ_BODY && c->read_pos >= c->body_len) {
c->state = PROCESS;
}
} else if (n == -1 && errno == EAGAIN) break;
else { handle_close(c); break; }
}
if (c->state == PROCESS) {
process_request(c); // 可能提交線程池
c->state = WRITE_HEAD;
mod_epoll(c, EPOLLOUT);
}
}
關鍵點:
EPOLLET + 非阻塞,必須讀寫至 EAGAIN,否則事件丟失。read_buf/write_buf 從內存池分配,支援鏈式緩存(ngx_chain_t 模式)。sendfile/splice/writev 減少用戶/核心態拷貝。| 坑點 | 後果 | 最佳實踐 |
|---|---|---|
| 全域 malloc/free 高併發 | CPU 100% 在 malloc 鎖、記憶體碎片 OOM | 全鏈路內存池、TLAB、大塊 mmap |
| 請求處理阻塞 I/O(DB、外部 API) | Worker 線程耗盡、服務雪崩 | 異步非阻塞、線程池隔離、熔斷降級 |
| 無連接/請求超時機制 | 死連佔盡 FD、記憶體洩漏 | 定時輪檢查、Idle Timeout、Read/Write Timeout |
| 日誌同步寫入、無級別控制 | 磁碟 IO 瓶頸、關鍵錯誤被淹沒 | 異步日誌、分級輸出、採樣、結構化 JSON |
| 協議無版本、無長度、無校驗 | 版本不兼容、粘包/拆包、數據損壞無感知 | TLV 格式、Magic Number、Version、CRC32、長度前綴 |
| 信號處理不當(SIGPIPE、SIGSEGV) | 進程莫名退出、Core Dump 無法分析 | signal(SIGPIPE, SIG_IGN)、SA_RESTART、Core Dump 目錄配置 |
| 配置熱載無驗證、無回滾 | 配置錯誤導致服務掛起 | 語法預檢、灰度發布、一鍵回滾、配置版本管理 |
Linux C/C++ 服務端開發的核心是資源控制權收攬:內存(池化)、線程(池化/工作盜)、I/O(epoll/非阻塞/零拷貝)、對象生命週期(引用計數/RCU)、協議(自描述/版本/校驗)。拒絕「寫代碼就上線」,建立「基礎設施先行、壓測驗證、可觀測完備」的工程文化,才能寫出經得起時間考驗的服務端系統。