- VMM cluster: XJ3-E/LIN-E/VMM-E/VMX-E/EPT-E/IPC-E/VVB-E (124 sites) - kernel cluster: TST-E/MM-E/UTRW-E/LAPIC-E/IDT-E/SAS-E/PRB-E/NETST-E/DKM-E (117 sites) - whitelist fixes: vmm_init env probes E10-E14 fallback, disk_log read-only block path, DSL-E05 non-FAT32 device 0 probe - QEMU smoke: wizard -> USER.CONF -> login -> desktop ready, zero spurious panic
327 lines
12 KiB
C
327 lines
12 KiB
C
/* virtio_net.c — UTSM virtio-net 设备模拟后端。
|
||
*
|
||
* 为 Linux guest 提供 virtio-net 网卡。guest 的收发包请求经 virtqueue
|
||
* 传入,本后端桥接到 UTSM 真实网卡(net API,e1000/virtio_net DKM 驱动)。
|
||
*
|
||
* Virtio Spec 1.1 §5.1 (Network Device)。
|
||
* 队列:0=RX(device→driver 收包),1=TX(driver→device 发包)。
|
||
* 每帧前置 virtio_net_hdr(10 字节,无 GSO/MRG_RXBUF 协商时全零)。
|
||
*
|
||
* 最小可用实现:
|
||
* - 无 GSO/TSO/checksum offload(feature 不声明,guest 走软件路径)
|
||
* - RX:guest 提交空 buffer,queue_notify / 周期 poll 时调 host rx_poll 填充
|
||
* - TX:queue_notify 时拼接描述符链成帧,直接 net->tx 发出
|
||
*/
|
||
|
||
#include <utsm/virtio_mmio.h>
|
||
#include <utsm/panic.h>
|
||
#include <utsm/linux_loader.h>
|
||
#include <utsm/net.h>
|
||
#include <utsm/log.h>
|
||
#include <utsm/types.h>
|
||
|
||
/* virtio_net_hdr(VIRTIO_NET_F_CSUM/GSO 未协商时为 10 字节 legacy) */
|
||
struct virtio_net_hdr {
|
||
u8 flags;
|
||
u8 gso_type;
|
||
u16 hdr_len;
|
||
u16 gso_size;
|
||
u16 csum_start;
|
||
u16 csum_offset;
|
||
/* u16 num_buffers; // 仅 VIRTIO_NET_F_MRG_RXBUF 协商后存在 */
|
||
} __attribute__((packed));
|
||
|
||
#define VIRTIO_NET_HDR_LEN 10
|
||
|
||
/* feature bits(除 MAC/STATUS 外都不声明,保持最简) */
|
||
#define VIRTIO_NET_F_CSUM 0
|
||
#define VIRTIO_NET_F_GUEST_CSUM 1
|
||
#define VIRTIO_NET_F_MAC 5
|
||
#define VIRTIO_NET_F_STATUS 16
|
||
|
||
/* device config(VIRTIO_MMIO_CONFIG 起) */
|
||
struct virtio_net_config {
|
||
u8 mac[6];
|
||
u16 status; /* bit0 = link up */
|
||
u16 max_virtqueue_pairs;
|
||
} __attribute__((packed));
|
||
|
||
static u8 g_net_mac[6] = { 0x52, 0x54, 0x00, 0xDE, 0x5A, 0x01 };
|
||
static int g_net_bound = -1;
|
||
|
||
/* ===== 数据路径统计(低频串口摘要,便于现场诊断 guest 网络问题) ===== */
|
||
static u64 g_stat_rx_filled; /* 已填充给 guest 的 RX 帧数 */
|
||
static u64 g_stat_rx_drop_nobuf; /* host 有包但 guest 无可用 RX buffer → 主动丢弃 */
|
||
static u64 g_stat_rx_badbuf; /* guest RX 描述符异常(未映射/hdr 太小/无 data 段) */
|
||
static u64 g_stat_tx_sent; /* 成功送交 host 网卡的 TX 帧数 */
|
||
static u64 g_stat_tx_drop; /* TX 丢弃帧数(链异常/超 MTU/host 发送失败) */
|
||
|
||
/* 每 512 次数据路径事件打印一行摘要(避免刷串口)。 */
|
||
static void net_stat_tick(void) {
|
||
u64 sum = g_stat_rx_filled + g_stat_rx_drop_nobuf + g_stat_rx_badbuf +
|
||
g_stat_tx_sent + g_stat_tx_drop;
|
||
if ((sum & 0x1FF) != 0) return;
|
||
log_hex64("[VNET] stat rx_filled=", g_stat_rx_filled);
|
||
log_hex64("[VNET] stat rx_drop_nobuf=", g_stat_rx_drop_nobuf);
|
||
log_hex64("[VNET] stat rx_badbuf=", g_stat_rx_badbuf);
|
||
log_hex64("[VNET] stat tx_sent=", g_stat_tx_sent);
|
||
log_hex64("[VNET] stat tx_drop=", g_stat_tx_drop);
|
||
}
|
||
|
||
static u32 net_read_config(u32 offset, int width) {
|
||
(void)width;
|
||
struct virtio_net_config cfg;
|
||
u8 *c = (u8 *)&cfg;
|
||
for (u32 i = 0; i < sizeof(cfg); i++) c[i] = 0;
|
||
for (int i = 0; i < 6; i++) cfg.mac[i] = g_net_mac[i];
|
||
cfg.status = 1; /* link up */
|
||
cfg.max_virtqueue_pairs = 1;
|
||
|
||
if (offset + 4 <= sizeof(cfg)) {
|
||
u8 *src = (u8 *)&cfg + offset;
|
||
return (u32)src[0] | ((u32)src[1] << 8) | ((u32)src[2] << 16) | ((u32)src[3] << 24);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 从 TX 描述符链拼接一个连续帧到 out_buf(out_cap 容量),返回帧长度。
|
||
* 第一个描述符前置 virtio_net_hdr:Linux 驱动把 hdr 作为【独立的 10 字节
|
||
* 描述符】(len 恰好等于 VIRTIO_NET_HDR_LEN),必须无条件跳过首描述符的
|
||
* 前 min(len,10) 字节,否则 hdr 会被拼进帧首导致 host 发出坏包。
|
||
* 任一段放不下 out_cap(超 MTU 截断)或描述符 GPA 未映射时返回 0(整帧丢弃)。 */
|
||
static u32 net_gather_frame(struct virtq_desc *desc, u16 head,
|
||
u8 *out_buf, u32 out_cap) {
|
||
u32 total = 0;
|
||
u16 cur = head;
|
||
int hops = 0;
|
||
while (hops < 32) {
|
||
u8 *src = (u8 *)virtio_gpa_to_host(desc[cur].addr);
|
||
u32 len = desc[cur].len;
|
||
if (!src) return 0;
|
||
|
||
/* 跳过首描述符中的 virtio_net_hdr(hdr 与数据同段时也正确) */
|
||
if (hops == 0) {
|
||
u32 skip = len < VIRTIO_NET_HDR_LEN ? len : VIRTIO_NET_HDR_LEN;
|
||
src += skip;
|
||
len -= skip;
|
||
}
|
||
if (len > 0) {
|
||
if (total + len > out_cap) return 0;
|
||
for (u32 i = 0; i < len; i++) out_buf[total + i] = src[i];
|
||
total += len;
|
||
}
|
||
|
||
if (!(desc[cur].flags & VIRTQ_DESC_F_NEXT)) break;
|
||
cur = desc[cur].next;
|
||
hops++;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
/* TX queue(idx=1):guest 发包。 */
|
||
static u16 g_tx_last_avail;
|
||
static u8 g_tx_scratch[2048];
|
||
|
||
static void net_handle_tx(void) {
|
||
struct virtq_desc *desc;
|
||
struct virtq_avail *avail;
|
||
struct virtq_used *used;
|
||
u32 qnum;
|
||
if (virtio_queue_get_ptrs(VIRTIO_ID_NET, 1, &desc, &avail, &used, &qnum) != 0)
|
||
return;
|
||
|
||
const dkm_net_api *net = net_get_api();
|
||
u16 cur = g_tx_last_avail;
|
||
u16 used_idx = used->idx;
|
||
while (cur != avail->idx) {
|
||
u16 head = avail->ring[cur % qnum];
|
||
u32 flen = 0;
|
||
if (head < qnum) {
|
||
flen = net_gather_frame(desc, head, g_tx_scratch, sizeof(g_tx_scratch));
|
||
}
|
||
if (flen > 0 && net && g_net_bound >= 0) {
|
||
if (net->tx((u32)g_net_bound, g_tx_scratch, flen) == 0) {
|
||
g_stat_tx_sent++;
|
||
} else {
|
||
/* host ring 满/发送失败:丢帧(guest 不感知 TX 成败) */
|
||
g_stat_tx_drop++;
|
||
}
|
||
} else {
|
||
/* 链异常/超 MTU 截断保护/未绑定网卡:丢帧 */
|
||
g_stat_tx_drop++;
|
||
}
|
||
/* TX buffer 必须回执(guest 回收 skb),len 字段对 TX 无意义 */
|
||
used->ring[used_idx % qnum].id = head;
|
||
used->ring[used_idx % qnum].len = 0;
|
||
used_idx++;
|
||
cur++;
|
||
net_stat_tick();
|
||
}
|
||
virtio_queue_bump_used(used, used_idx);
|
||
g_tx_last_avail = cur;
|
||
}
|
||
|
||
/* RX queue(idx=0):guest 提交空 buffer,我们调 host rx_poll 填充。
|
||
*
|
||
* buffer 所有权规则(关键,避免泄漏):
|
||
* - guest 一次性投满 RX 队列后只 notify 一次,之后等 used 事件。
|
||
* - 有包:填充 hdr+data → used ring 回执 → guest 收包后重投 buffer。
|
||
* - 无包:【必须停止且不消耗该 buffer】(g_rx_last_avail 保持指向它),
|
||
* 由 virtio_net_poll() 在 preemption timer 路径周期性重试;
|
||
* 否则 buffer 被跳过且永远不进 used ring → guest RX buffer 耗尽饿死。
|
||
* - guest 无可用 buffer 时:主动把 host 收到的包收出来丢弃并统计,
|
||
* 避免 e1000 8 深 RX ring 堵死后网卡级无差别丢包(virtio 语义上
|
||
* device 在 RX 无 buffer 时丢包是标准行为)。
|
||
* 填充 ≥1 个 buffer 后由调用方负责注入 guest IRQ。 */
|
||
static u16 g_rx_last_avail;
|
||
static u8 g_rx_drop_scratch[2048]; /* 无 guest buffer 时的丢弃暂存 */
|
||
|
||
/* 解析 RX 描述符链的 data 接收区(跳过首描述符的 hdr 区)。
|
||
* 支持两种布局:
|
||
* A. hdr(10B, W) + data(N, W) 两个描述符(Linux add_recvbuf_small 布局)
|
||
* B. hdr+data 同在一个描述符(len > 10,data 从偏移 10 起)
|
||
* 成功返回 0 并填充 datap/datacap/hdrp 输出参数;失败返回 -1(链异常)。 */
|
||
static int net_rx_data_region(struct virtq_desc *desc, u16 head,
|
||
u8 **datap, u32 *datacap, u8 **hdrp) {
|
||
struct virtq_desc *d0 = &desc[head];
|
||
u8 *hdr = (u8 *)virtio_gpa_to_host(d0->addr);
|
||
if (!hdr || d0->len < VIRTIO_NET_HDR_LEN) return -1;
|
||
*hdrp = hdr;
|
||
|
||
if (d0->len > VIRTIO_NET_HDR_LEN) {
|
||
/* 布局 B:hdr 与 data 同段 */
|
||
if (!(d0->flags & VIRTQ_DESC_F_WRITE)) return -1;
|
||
*datap = hdr + VIRTIO_NET_HDR_LEN;
|
||
*datacap = d0->len - VIRTIO_NET_HDR_LEN;
|
||
return 0;
|
||
}
|
||
/* 布局 A:hdr 独立描述符,data 在 NEXT 段 */
|
||
if (!(d0->flags & VIRTQ_DESC_F_NEXT)) return -1;
|
||
struct virtq_desc *d1 = &desc[d0->next];
|
||
if (!(d1->flags & VIRTQ_DESC_F_WRITE)) return -1;
|
||
u8 *data = (u8 *)virtio_gpa_to_host(d1->addr);
|
||
if (!data || d1->len == 0) return -1;
|
||
*datap = data;
|
||
*datacap = d1->len;
|
||
return 0;
|
||
}
|
||
|
||
/* 处理 RX 队列。返回本次填充的 buffer 数(0 = 无包/无可填)。 */
|
||
static int net_handle_rx(void) {
|
||
struct virtq_desc *desc;
|
||
struct virtq_avail *avail;
|
||
struct virtq_used *used;
|
||
u32 qnum;
|
||
if (virtio_queue_get_ptrs(VIRTIO_ID_NET, 0, &desc, &avail, &used, &qnum) != 0)
|
||
return 0;
|
||
|
||
const dkm_net_api *net = net_get_api();
|
||
if (!net || g_net_bound < 0) return 0;
|
||
|
||
int filled = 0;
|
||
u16 cur = g_rx_last_avail;
|
||
u16 used_idx = used->idx;
|
||
int budget = 32; /* 单次调用处理上限,避免 vmexit/poll 路径停留过久 */
|
||
|
||
while (budget-- > 0) {
|
||
if (cur == avail->idx) {
|
||
/* guest 无可用 RX buffer:主动收包丢弃,防 host RX ring 堵死 */
|
||
u32 got = 0;
|
||
if (net->rx_poll((u32)g_net_bound, g_rx_drop_scratch,
|
||
sizeof(g_rx_drop_scratch), &got) == 0 && got > 0) {
|
||
g_stat_rx_drop_nobuf++;
|
||
net_stat_tick();
|
||
continue;
|
||
}
|
||
break; /* host 也无包 */
|
||
}
|
||
|
||
u16 head = avail->ring[cur % qnum];
|
||
u8 *hdrp = (u8 *)0;
|
||
u8 *datap = (u8 *)0;
|
||
u32 datacap = 0;
|
||
if (head >= qnum ||
|
||
net_rx_data_region(desc, head, &datap, &datacap, &hdrp) != 0) {
|
||
/* 异常 buffer:len=0 回执让 guest 回收重投,避免卡死 */
|
||
used->ring[used_idx % qnum].id = head;
|
||
used->ring[used_idx % qnum].len = 0;
|
||
used_idx++;
|
||
cur++;
|
||
filled++;
|
||
g_stat_rx_badbuf++;
|
||
net_stat_tick();
|
||
continue;
|
||
}
|
||
|
||
u32 got = 0;
|
||
if (net->rx_poll((u32)g_net_bound, datap, datacap, &got) == 0 && got > 0) {
|
||
/* 填 virtio_net_hdr 全零(无 GSO) */
|
||
for (int i = 0; i < VIRTIO_NET_HDR_LEN; i++) hdrp[i] = 0;
|
||
used->ring[used_idx % qnum].id = head;
|
||
used->ring[used_idx % qnum].len = VIRTIO_NET_HDR_LEN + got;
|
||
used_idx++;
|
||
cur++;
|
||
filled++;
|
||
g_stat_rx_filled++;
|
||
net_stat_tick();
|
||
} else {
|
||
/* 无包:停止,不消耗 cur 指向的 buffer,下次 poll 重试 */
|
||
break;
|
||
}
|
||
}
|
||
virtio_queue_bump_used(used, used_idx);
|
||
g_rx_last_avail = cur;
|
||
return filled;
|
||
}
|
||
|
||
static void net_queue_notify(u32 queue_idx) {
|
||
if (queue_idx == 1) net_handle_tx();
|
||
else if (queue_idx == 0) net_handle_rx();
|
||
}
|
||
|
||
static void net_reset(void) {
|
||
g_tx_last_avail = 0;
|
||
g_rx_last_avail = 0;
|
||
}
|
||
|
||
static struct virtio_backend g_net_backend = {
|
||
.device_id = VIRTIO_ID_NET,
|
||
.gpa_base = VIRTIO_MMIO_NET_GPA,
|
||
.irq = 6, /* guest ISA IRQ6(cmdline :6,vector 0x36) */
|
||
.num_queues = 2,
|
||
.queue_size = 128,
|
||
.device_features = (1ULL << VIRTIO_F_VERSION_1) |
|
||
(1ULL << VIRTIO_NET_F_MAC) | (1ULL << VIRTIO_NET_F_STATUS),
|
||
.config_len = sizeof(struct virtio_net_config),
|
||
.read_config = net_read_config,
|
||
.queue_notify = net_queue_notify,
|
||
.reset = net_reset,
|
||
};
|
||
|
||
void virtio_net_backend_init(void) {
|
||
/* 绑定 UTSM 第 0 个网卡(e1000 或 virtio_net DKM 驱动) */
|
||
const dkm_net_api *net = net_get_api();
|
||
if (net && net->device_count() > 0) {
|
||
g_net_bound = 0;
|
||
dkm_net_device_info info;
|
||
if (net->device_info(0, &info) == 0) {
|
||
for (int i = 0; i < 6; i++) g_net_mac[i] = info.mac[i];
|
||
}
|
||
log_info("[VNET] bound to UTSM net device 0");
|
||
} else {
|
||
g_net_bound = -1;
|
||
log_warn("[VNET] no UTSM net device, net backend degraded");
|
||
}
|
||
virtio_mmio_register(&g_net_backend);
|
||
}
|
||
|
||
/* 周期轮询入口(vmexit preemption timer 路径调用,~1kHz)。
|
||
* guest 投满 RX buffer 后不再 notify,host 侧包到达时靠此轮询填充
|
||
* 并注入 IRQ,guest 才能及时收包(pacman/ping 等依赖)。 */
|
||
void virtio_net_poll(void) {
|
||
if (g_net_bound < 0) return;
|
||
if (net_handle_rx() > 0) {
|
||
virtio_mmio_raise_irq(VIRTIO_ID_NET);
|
||
}
|
||
}
|