Linux下如何压满网卡带宽及关键影响因素分析
1. 压满网卡带宽的含义
在常见的客户端-服务端通信场景中,当监控工具显示客户端的上行带宽和服务端的下行带宽均达到上限,即视为网卡带宽被压满。
2. 测试工具与脚本
客户端代码(C语言,基于Socket):
// 客户端程序,用于发送数据
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <sched.h>
#define PORT 8080
#define BUF_SIZE (4*1024)
#define SEND_BUF_SIZE (2*1024)
void busy_work(int iterations) {
static int seed = 1;
double result = 0.0;
for (int i = 0; i < iterations; i++) {
result = result * 1223491.3 * (double)(9401 + i) * seed++;
}
}
int main(int argc, char* argv[]) {
short port = atoi(argv[1]);
int send_size = atoi(argv[2]);
int sleep_us = atoi(argv[3]);
int work_iters = atoi(argv[4]);
int sock_fd;
struct sockaddr_in server_addr;
char* data = malloc(send_size);
for (int i = 0; i < send_size; i++) data[i] = '0' + (i % 10);
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) { perror("socket failed"); return -1; }
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, "10.10.3.93", &server_addr.sin_addr);
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed"); return -1;
}
while (1) {
busy_work(work_iters);
send(sock_fd, data, send_size, 0);
}
close(sock_fd);
return 0;
}
客户端启动命令示例:
nohup ./client 8080 2048 0 1000 &
nohup ./client 8081 2048 0 1000 &
nohup ./client 8082 2048 0 1000 &
服务端代码(C语言,基于Socket):
// 服务端程序,用于接收数据
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <sched.h>
#define PORT 8080
#define BUF_SIZE (4*1024)
void busy_work(int iterations) {
static int seed = 1;
double result = 0.0;
for (int i = 0; i < iterations; i++) {
result = result * 1223491.3 * (double)(9401 + i) * seed++;
}
}
int main(int argc, char* argv[]) {
short port = atoi(argv[1]);
int sleep_us = atoi(argv[2]);
int work_iters = atoi(argv[3]);
int server_fd, client_fd;
struct sockaddr_in address;
int opt = 1;
socklen_t addr_len = sizeof(address);
char buf[BUF_SIZE];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
listen(server_fd, 3);
client_fd = accept(server_fd, (struct sockaddr*)&address, &addr_len);
while (1) {
int n = read(client_fd, buf, sizeof(buf));
busy_work(work_iters);
if (n <= 0) { printf("read error"); exit(-2); }
}
close(client_fd);
close(server_fd);
return 0;
}
服务端启动命令示例:
nohup ./server 8080 0 1000 &
nohup ./server 8081 0 1000 &
nohup ./server 8082 0 1000 &
3. 测试结果
测试环境为同一宿主机(使用虚拟网卡),结果如下:
- usleep 100 微秒:约 300 Mb/s
- usleep 10 微秒:约 600 Mb/s
- usleep 1 微秒:约 690 Mb/s
- 空循环(empty loop):约 20 Gb/s
- usleep 0 微秒:约 708 Mb/s(触发deactivate_task调用)
- sched_yield:约 10 Gb/s
- 循环10000次复杂乘法(static变量避免缓存):约 2.7 Gb/s
- 循环1000次复杂乘法:约 8.2 Gb/s
- 循环100次复杂乘法:约 15 Gb/s
4. 初步分析与结论
- 使用空循环可以轻松压满带宽。
- usleep(0)虽然看似不耗时,但内部会触发deactivate_task等系统调用,导致带宽显著下降。
- sched_yield通过主动让出CPU,仍然可以达到较高的带宽。
- 即使执行大量CPU计算(如循环乘法),带宽仍然可以保持较高水平。
5. 总结
影响网卡带宽压满的关键因素不仅包括代码中的耗时操作,更重要的是操作系统接口(如usleep)所引入的内核态切换开销。选择合适的时间控制策略对于最大化网络吞吐至关重要。