在物联网(IoT)和工业4.0的推动下,嵌入式设备逐渐从单机控制转向网络互联。然而,嵌入式系统的资源限制(如内存、CPU性能)与复杂的网络环境(高延迟、低带宽)对网络编程提出了严峻挑战。
核心痛点:
// 基础TCP服务器代码框架
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind & listen ...
while (1) {
int connfd = accept(listenfd, NULL, NULL); // 阻塞等待连接
pid_t pid = fork();
if (pid == 0) { // 子进程处理连接
close(listenfd);
handle_client(connfd);
exit(0);
}
close(connfd);
}
}
缺陷:
fd_set readfds;
int maxfd = listenfd;
FD_SET(listenfd, &readfds);
while (1) {
fd_set tmpfds = readfds;
int nready = select(maxfd + 1, &tmpfds, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &tmpfds)) {
int connfd = accept(listenfd, NULL, NULL);
FD_SET(connfd, &readfds);
maxfd = (connfd > maxfd) ? connfd : maxfd;
}
for (int fd = listenfd + 1; fd <= maxfd; fd++) {
if (FD_ISSET(fd, &tmpfds)) {
handle_client(fd); // 非阻塞处理
}
}
}
优势:单线程处理多连接;
问题:
#include
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件
#define MAX_EVENTS 1024
int epollfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
// 添加监听套接字到epoll
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nready; i++) {
if (events[i].data.fd == listenfd) {
int connfd = accept(listenfd, NULL, NULL);
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = connfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
} else {
handle_client(events[i].data.fd);
}
}
}
性能优势:
嵌入式设备内存有限,需避免频繁的malloc/free。
解决方案:预分配固定大小的内存块。
#define POOL_SIZE 1024
#define BLOCK_SIZE 2048
char memory_pool[POOL_SIZE][BLOCK_SIZE];
int free_blocks[POOL_SIZE];
int top = POOL_SIZE - 1;
// 初始化内存池
void init_pool() {
for (int i = 0; i < POOL_SIZE; i++) {
free_blocks[i] = i;
}
}
// 分配内存块
char* alloc_block() {
if (top < 0) return NULL;
return memory_pool[free_blocks[top--]];
}
// 释放内存块
void free_block(int index) {
free_blocks[++top] = index;
}
使用sendfile和splice减少内核态与用户态的数据拷贝。
// 发送文件内容到套接字(零拷贝)
int send_file(int sockfd, const char* filename) {
int filefd = open(filename, O_RDONLY);
off_t offset = 0;
struct stat filestat;
fstat(filefd, &filestat);
ssize_t sent = sendfile(sockfd, filefd, &offset, filestat.st_size);
close(filefd);
return sent;
}
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
// 设置套接字非阻塞
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
// 处理客户端请求
void handle_client(int fd) {
char buffer[BUFFER_SIZE];
ssize_t n = read(fd, buffer, BUFFER_SIZE);
if (n > 0) {
write(fd, buffer, n); // 回显数据
} else if (n == 0 || (n < 0 && errno != EAGAIN)) {
close(fd);
}
}
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(8080)
};
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, SOMAXCONN);
int epollfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listenfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epollfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nready; i++) {
if (events[i].data.fd == listenfd) {
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len);
set_nonblocking(connfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
} else {
handle_client(events[i].data.fd);
}
}
}
return 0;
}
边缘触发模式(EPOLLET):
需一次性读取所有数据,否则可能丢失事件通知。
void handle_client(int fd) {
char buffer[BUFFER_SIZE];
while (1) { // 循环读取直到EAGAIN
ssize_t n = read(fd, buffer, BUFFER_SIZE);
if (n <= 0) break;
write(fd, buffer, n);
}
}
非阻塞I/O:
避免单个连接的阻塞导致整个服务停滞。
内存池集成:
替换buffer
为预分配内存块,减少动态内存分配。
# 安装wrk
sudo apt-get install wrk
# 启动测试(100并发,持续30秒)
wrk -t4 -c100 -d30s http://192.168.1.100:8080
模型 | 连接数 | QPS | 内存占用(MB) | CPU使用率 |
---|---|---|---|---|
多进程 | 100 | 1200 | 50 | 90% |
select | 1000 | 8500 | 15 | 75% |
epoll(默认) | 1000 | 23000 | 10 | 60% |
epoll+零拷贝 | 1000 | 35000 | 8 | 45% |
调整内核参数:
# 增大本地端口范围
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 增加最大打开文件数
sysctl -w fs.file-max=1000000
启用TCP快速打开(TFO):
int qlen = 5;
setsockopt(listenfd, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
使用多线程epoll:
将连接分配到多个epoll实例,充分利用多核CPU。
本文从传统多进程模型出发,逐步演进到epoll高并发方案,结合嵌入式系统的特性,实现了资源高效利用的TCP服务器。进一步研究方向: