C语言HTTP服务器高并发下socket读取内存耗尽问题咨询
我用C语言写了一个非阻塞Socket的HTTP服务器,在100-500个并发客户端时运行正常,但面对大量并发用户(比如1000个)时很快出现内存耗尽的情况,报错Cannot allocate memory (error code: 12),同时伴随大量HTTP 500错误。
处理客户端请求的核心代码
int handle_client(char *root, int c) { int size = sizeof(char) * BUFSIZ + 1, len = strlen(SEND_HTTP_NOT_FOUND); char *buffer = malloc(size), *data = NULL, not_found_error[200] = SEND_HTTP_NOT_FOUND, internal_error[200] = SEND_HTTP_INTERNAL_SERVER_ERROR; if (!buffer) { log_error(errno); return -1; } bzero(buffer, size); while (1) { if (read(c, buffer, size) <= 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { log_error(errno); free(buffer); if (c) close(c); return -1; } else { int not_found = 0; data = parse_content(root, buffer, not_found); if (!data) { if (not_found) { if (write(c, not_found_error, len) <= 0) log_error(errno); } else { if (write(c, internal_error, len) <= 0) log_error(errno); } free(buffer); if (c) close(c); return -1; } break; } } } if (write(c, data, strlen(data)) <= 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { log_error(errno); close(c); free(data); free(buffer); return -1; } } close(c); free(data); free(buffer); return 0; }
测试场景
使用siege工具压测:
siege --file=try_me.txt -c 1000 -v
其中try_me.txt包含两个请求URL:/和/test.txt。
我有三个问题:
- 无法分配内存的对象是什么?
- 为什么会出现无法分配内存的情况?
- 如何正确为其分配内存?
让我一步步帮你拆解这个问题,从根源到解决方案逐个说明:
1. 无法分配内存的对象是什么?
从报错位置和代码逻辑来看,直接触发内存分配失败的是handle_client函数开头malloc(size)创建的buffer内存块。这个buffer的大小是BUFSIZ + 1(通常BUFSIZ默认是8192字节,单块内存就占8KB左右),它是函数里第一个动态内存分配操作,也是高并发下最容易触发分配失败的点——因为每个并发请求都会独立申请这块内存。
2. 为什么会出现无法分配内存的情况?
核心问题出在高并发下的内存管理策略缺陷,具体有几个关键点:
- 并发请求的内存叠加:当1000个并发连接同时处理时,每个请求都要申请8KB的
buffer,光是这部分就需要8MB内存。如果你的服务器是基于多线程/多进程模型,或者事件循环中没有及时回收内存,这个数字会快速膨胀,超出系统可用内存。 - 非阻塞Socket的读取逻辑错误:你当前的逻辑是只要
read返回EAGAIN/EWOULDBLOCK就直接解析buffer,但非阻塞Socket的read经常只能读取部分请求数据——用不完整的请求去调用parse_content,大概率会解析失败,触发错误分支。如果parse_content内部有未正确释放的内存,就会导致内存泄漏,积累多了就会耗尽系统内存。 - 内存碎片化:频繁的
malloc和free会让系统内存变得碎片化——虽然总空闲内存足够,但没有连续的8KB块来满足分配请求,也会触发Cannot allocate memory错误。 - 潜在的内存泄漏分支:比如在
read返回非EAGAIN错误时,虽然释放了buffer,但如果parse_content已经分配了data内存,这个分支里没有释放data(不过你的代码里这个分支data还没赋值,暂时没问题,但其他分支要注意);另外如果write返回EAGAIN后直接关闭连接,若后续事件循环又重新处理这个连接,会重复分配内存。
3. 如何正确为其分配内存?
针对你的场景,给出几个切实可行的优化方案:
① 用内存池复用内存,减少碎片化
不要为每个请求都调用malloc/free,提前批量分配一批固定大小的内存块,用链表管理空闲池。处理请求时从池里取,处理完放回池里,既提升分配效率,又避免碎片化。
示例代码思路:
// 定义内存池节点 typedef struct BufferNode { char buf[BUFSIZ + 1]; struct BufferNode *next; } BufferNode; static BufferNode *free_pool = NULL; static pthread_mutex_t pool_lock = PTHREAD_MUTEX_INITIALIZER; // 从内存池获取缓冲区 char* get_buffer() { pthread_mutex_lock(&pool_lock); // 空闲池为空时批量分配100个节点 if (!free_pool) { for (int i = 0; i < 100; i++) { BufferNode *node = malloc(sizeof(BufferNode)); if (!node) { pthread_mutex_unlock(&pool_lock); return NULL; } node->next = free_pool; free_pool = node; } } BufferNode *node = free_pool; free_pool = node->next; pthread_mutex_unlock(&pool_lock); bzero(node->buf, BUFSIZ + 1); return node->buf; } // 归还缓冲区到内存池 void put_buffer(char *buf) { BufferNode *node = (BufferNode*)((char*)buf - offsetof(BufferNode, buf)); pthread_mutex_lock(&pool_lock); node->next = free_pool; free_pool = node; pthread_mutex_unlock(&pool_lock); }
然后在handle_client里把malloc(size)换成get_buffer(),free(buffer)换成put_buffer(buffer)即可。
② 修复非阻塞Socket的读取逻辑
非阻塞Socket不能一次性读完所有数据,你需要为每个连接维护一个上下文,保存已读取的数据和状态,直到读取到完整的HTTP请求(比如读到\r\n\r\n标志)。
示例调整后的读取逻辑:
// 为每个连接定义上下文结构体 typedef struct ClientCtx { int fd; char buffer[BUFSIZ + 1]; int read_pos; // 已读取数据的长度 } ClientCtx; // 在处理连接的逻辑中 ClientCtx *ctx = malloc(sizeof(ClientCtx)); if (!ctx) { /* 错误处理 */ } ctx->fd = c; ctx->read_pos = 0; bzero(ctx->buffer, sizeof(ctx->buffer)); while (1) { ssize_t n = read(ctx->fd, ctx->buffer + ctx->read_pos, sizeof(ctx->buffer) - ctx->read_pos - 1); if (n > 0) { ctx->read_pos += n; ctx->buffer[ctx->read_pos] = '\0'; // 检查是否读取到完整的HTTP请求 if (strstr(ctx->buffer, "\r\n\r\n")) { // 解析并处理请求 int not_found = 0; char *data = parse_content(root, ctx->buffer, ¬_found); // 后续响应发送逻辑... break; } } else if (n == 0) { // 客户端关闭连接 free(ctx); close(c); return -1; } else { if (errno != EAGAIN && errno != EWOULDBLOCK) { // 发生错误 free(ctx); close(c); return -1; } else { // 无数据可读,等待下一次事件触发 break; } } }
这样可以避免不完整请求导致的解析失败和内存浪费,也减少了不必要的内存分配。
③ 确保所有分支的内存都被正确释放
仔细检查handle_client的所有退出路径:
- 当
parse_content返回NULL时,确保如果data已经被分配,要释放; - 当
write返回EAGAIN时,不要直接关闭连接,而是把连接放回事件循环,等待可写事件触发后再发送响应,避免重复分配内存处理同一个请求; - 所有动态分配的内存(
buffer、data、ClientCtx)在任何退出分支都要被释放。
④ 按需调整缓冲区大小
如果你的HTTP请求大多比较小(比如常见的GET请求只有几百字节),可以把BUFSIZ换成更小的值(比如2048),减少每个请求的内存占用,缓解高并发下的内存压力。
内容的提问来源于stack exchange,提问作者ssharma

