From: Yufeng Wang <[email protected]>

Implement SQ/CQ doorbell polling in the vhost kernel backend:

  - vhost.c: per-VQ poll thread (kthread) that polls sq->idx for
    submissions and processes completions via poll_complete callback;
    need_resched() cooperative spin; round-robin CPU binding with
    modulo to prevent overflow; sqcq_poll enabled after poll_task is
    valid (NULL deref fix); get_user fault retry (3 attempts, 10ms
    backoff); EMA overflow-safe formula
  - vhost.h: vhost_virtqueue extensions (sq/cq pointers, poll thread
    state, EMA fields)
  - scsi.c: vhost_scsi_poll_complete() writes cq->idx and signals
    Guest; SET_ENDPOINT error path stops started poll threads (fallback);
    10s window diagnostic output
  - vhost.h UAPI: VHOST_SET_VRING_SQCQ_ADDR ioctl
  - vhost_types.h UAPI: struct vhost_vring_sqcq_addr

Signed-off-by: Yufeng Wang <[email protected]>
---
 Documentation/compare-sqcq-results.sh         | 239 +++++++
 Documentation/run-sqcq-compare.sh             | 182 ++++++
 Documentation/virtio-sqcq-poll-spec.rst       | 617 ++++++++++++++++++
 .../virtio-sqcq-poll-virtio-spec.rst          | 355 ++++++++++
 build-riscv-kernel.sh                         |  26 +
 drivers/vhost/scsi.c                          | 187 +++++-
 drivers/vhost/vhost.c                         | 595 ++++++++++++++++-
 drivers/vhost/vhost.h                         |  29 +-
 include/uapi/linux/vhost.h                    |   4 +
 include/uapi/linux/vhost_types.h              |   6 +
 10 files changed, 2221 insertions(+), 19 deletions(-)
 create mode 100644 Documentation/compare-sqcq-results.sh
 create mode 100644 Documentation/run-sqcq-compare.sh
 create mode 100644 Documentation/virtio-sqcq-poll-spec.rst
 create mode 100644 Documentation/virtio-sqcq-poll-virtio-spec.rst
 create mode 100644 build-riscv-kernel.sh

diff --git a/Documentation/compare-sqcq-results.sh 
b/Documentation/compare-sqcq-results.sh
new file mode 100644
index 0000000000000..85bb927fe91e5
--- /dev/null
+++ b/Documentation/compare-sqcq-results.sh
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+"""compare-sqcq-results.py - Generate A/B comparison report for SQ/CQ polling 
vs baseline.
+
+Usage:
+    python3 compare-sqcq-results.py <baseline_dir> <sqcq_dir>
+    python3 compare-sqcq-results.py   # auto-detect latest results
+"""
+import json, sys, os, glob, re
+from collections import OrderedDict
+
+def find_latest_dirs():
+    """Find the most recent baseline and sqcq-poll result directories."""
+    base = "perf-results"
+    if not os.path.isdir(base):
+        print(f"Error: '{base}/' not found. Run run-sqcq-compare.sh first.")
+        sys.exit(1)
+
+    bl_dirs = sorted(glob.glob(os.path.join(base, "baseline_*")))
+    sq_dirs = sorted(glob.glob(os.path.join(base, "sqcq-poll_*")))
+
+    if not bl_dirs:
+        print("Error: No baseline_* directory found in perf-results/")
+        sys.exit(1)
+    if not sq_dirs:
+        print("Error: No sqcq-poll_* directory found in perf-results/")
+        sys.exit(1)
+
+    return bl_dirs[-1], sq_dirs[-1]
+
+
+def parse_fio_json(filepath):
+    """Extract key metrics from a fio JSON output file."""
+    with open(filepath) as f:
+        d = json.load(f)
+
+    jobs = d.get("jobs", [])
+    if not jobs:
+        return None
+
+    j = jobs[0]
+    rd = j.get("read", {})
+    wr = j.get("write", {})
+
+    # Determine which direction has data
+    if rd.get("iops", 0) > 0:
+        data = rd
+    else:
+        data = wr
+
+    iops = data.get("iops", 0)
+    bw_mib = data.get("bw", 0) / 1024  # KiB/s -> MiB/s
+
+    # Latency
+    lat_ns = data.get("lat_ns", {})
+    avg_us = lat_ns.get("mean", 0) / 1000
+    min_us = lat_ns.get("min", 0) / 1000
+    max_us = lat_ns.get("max", 0) / 1000
+    stddev_us = lat_ns.get("stddev", 0) / 1000
+
+    # Completion latency percentiles
+    clat = data.get("clat_ns", {})
+    pct = clat.get("percentile", {}) if isinstance(clat, dict) else {}
+    p50 = pct.get("50.000000", 0) / 1000
+    p95 = pct.get("95.000000", 0) / 1000
+    p99 = pct.get("99.000000", 0) / 1000
+    p999 = pct.get("99.900000", 0) / 1000
+
+    return {
+        "iops": iops,
+        "bw_mib": bw_mib,
+        "lat_avg_us": avg_us,
+        "lat_min_us": min_us,
+        "lat_max_us": max_us,
+        "lat_stddev_us": stddev_us,
+        "p50_us": p50,
+        "p95_us": p95,
+        "p99_us": p99,
+        "p999_us": p999,
+    }
+
+
+def pct_change(old, new):
+    """Calculate percentage change. Returns None if division by zero."""
+    if old == 0:
+        return None
+    return (new - old) / old * 100
+
+
+def fmt_pct(val):
+    """Format a percentage change value."""
+    if val is None:
+        return "N/A"
+    sign = "+" if val > 0 else ""
+    return f"{sign}{val:.1f}%"
+
+
+def fmt_int(val):
+    """Format integer with comma separators."""
+    return f"{val:,.0f}"
+
+
+def fmt_f1(val):
+    """Format float with 1 decimal."""
+    return f"{val:.1f}"
+
+
+def main():
+    if len(sys.argv) >= 3:
+        bl_dir = sys.argv[1]
+        sq_dir = sys.argv[2]
+    else:
+        bl_dir, sq_dir = find_latest_dirs()
+
+    print(f"Baseline:   {bl_dir}")
+    print(f"SQ/CQ Poll: {sq_dir}")
+    print()
+
+    # Collect all test labels
+    bl_files = {os.path.basename(f).replace("_best.json", ""): f
+                for f in glob.glob(os.path.join(bl_dir, "*_best.json"))}
+    sq_files = {os.path.basename(f).replace("_best.json", ""): f
+                for f in glob.glob(os.path.join(sq_dir, "*_best.json"))}
+
+    all_labels = sorted(set(bl_files.keys()) | set(sq_files.keys()))
+
+    if not all_labels:
+        print("Error: No *_best.json files found.")
+        sys.exit(1)
+
+    # Define metric display columns
+    metrics = [
+        ("IOPS",        "iops",        fmt_int,  True),
+        ("BW(MiB/s)",   "bw_mib",      fmt_f1,   True),
+        ("Avg(us)",     "lat_avg_us",  fmt_f1,   False),
+        ("P50(us)",     "p50_us",      fmt_f1,   False),
+        ("P95(us)",     "p95_us",      fmt_f1,   False),
+        ("P99(us)",     "p99_us",      fmt_f1,   False),
+        ("P99.9(us)",   "p999_us",     fmt_f1,   False),
+    ]
+
+    # Per-label comparison
+    results = []
+    for label in all_labels:
+        bl_data = parse_fio_json(bl_files[label]) if label in bl_files else 
None
+        sq_data = parse_fio_json(sq_files[label]) if label in sq_files else 
None
+
+        entry = {"label": label, "baseline": bl_data, "sqcq": sq_data}
+        results.append(entry)
+
+    # Print grouped by workload type
+    groups = OrderedDict()
+    for label in all_labels:
+        # Extract workload type from label: e.g. "4k-randread-od1-nj1" -> 
"4k-randread"
+        parts = label.rsplit("-od", 1)
+        wl = parts[0] if len(parts) > 1 else label
+        if wl not in groups:
+            groups[wl] = []
+        groups[wl].append(label)
+
+    print("=" * 100)
+    print("  SQ/CQ POLL vs BASELINE COMPARISON REPORT")
+    print("=" * 100)
+
+    for wl, labels in groups.items():
+        print(f"\n{'─' * 100}")
+        print(f"  Workload: {wl}")
+        print(f"{'─' * 100}")
+
+        for label in labels:
+            r = next(x for x in results if x["label"] == label)
+            bl = r["baseline"]
+            sq = r["sqcq"]
+
+            # Extract iodepth/numjobs from label
+            m = re.search(r'od(\d+)-nj(\d+)', label)
+            od = m.group(1) if m else "?"
+            nj = m.group(2) if m else "?"
+
+            print(f"\n  iodepth={od}, numjobs={nj}")
+            print(f"  {'Metric':<14} {'Baseline':>12} {'SQ/CQ Poll':>12} 
{'Change':>10}")
+            print(f"  {'─'*14} {'─'*12} {'─'*12} {'─'*10}")
+
+            for metric_name, metric_key, fmt_fn, higher_is_better in metrics:
+                bl_val = bl[metric_key] if bl else 0
+                sq_val = sq[metric_key] if sq else 0
+                change = pct_change(bl_val, sq_val)
+
+                # For latency, negative change is improvement
+                line = f"  {metric_name:<14} {fmt_fn(bl_val):>12} 
{fmt_fn(sq_val):>12}"
+
+                if change is not None:
+                    marker = ""
+                    if higher_is_better and change > 2:
+                        marker = " *"
+                    elif not higher_is_better and change < -2:
+                        marker = " *"
+                    line += f" {fmt_pct(change):>10}{marker}"
+                else:
+                    line += f" {'N/A':>10}"
+
+                print(line)
+
+    # Summary table - just IOPS
+    print(f"\n{'=' * 100}")
+    print("  IOPS SUMMARY (all scenarios)")
+    print(f"{'=' * 100}")
+    print(f"\n  {'Test':<30} {'Baseline':>10} {'SQ/CQ':>10} {'Change':>10}")
+    print(f"  {'─'*30} {'─'*10} {'─'*10} {'─'*10}")
+
+    for r in results:
+        bl_iops = r["baseline"]["iops"] if r["baseline"] else 0
+        sq_iops = r["sqcq"]["iops"] if r["sqcq"] else 0
+        change = pct_change(bl_iops, sq_iops)
+
+        marker = " **" if change and change > 5 else ""
+        print(f"  {r['label']:<30} {bl_iops:>10,.0f} {sq_iops:>10,.0f} 
{fmt_pct(change):>10}{marker}")
+
+    # Latency summary
+    print(f"\n{'=' * 100}")
+    print("  P99 LATENCY SUMMARY (all scenarios)")
+    print(f"{'=' * 100}")
+    print(f"\n  {'Test':<30} {'Base(p99)':>12} {'SQ/CQ(p99)':>12} 
{'Change':>10}")
+    print(f"  {'─'*30} {'─'*12} {'─'*12} {'─'*10}")
+
+    for r in results:
+        bl_p99 = r["baseline"]["p99_us"] if r["baseline"] else 0
+        sq_p99 = r["sqcq"]["p99_us"] if r["sqcq"] else 0
+        change = pct_change(bl_p99, sq_p99)
+
+        marker = " **" if change and change < -5 else ""
+        print(f"  {r['label']:<30} {bl_p99:>12.1f} {sq_p99:>12.1f} 
{fmt_pct(change):>10}{marker}")
+
+    print(f"\n  *  = notable improvement")
+    print(f"  ** = significant improvement (>5% IOPS gain or >5% latency 
reduction)")
+    print()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/Documentation/run-sqcq-compare.sh 
b/Documentation/run-sqcq-compare.sh
new file mode 100644
index 0000000000000..3a5928bfaef2a
--- /dev/null
+++ b/Documentation/run-sqcq-compare.sh
@@ -0,0 +1,182 @@
+#!/bin/bash
+# run-sqcq-compare.sh - A/B comparison for SQ/CQ polling vs baseline
+#
+# Usage:
+#   1. Boot Guest WITHOUT  VIRTIO_F_SQCQ_POLL:  ./run-sqcq-compare.sh baseline
+#   2. Boot Guest WITH     VIRTIO_F_SQCQ_POLL:  ./run-sqcq-compare.sh sqcq-poll
+#
+# Results saved to: perf-results/<mode>_<timestamp>/
+#
+# IMPORTANT: CPU pinning uses --cpus_allowed (NOT taskset).
+#            Verify with: ps -eo pid,comm,psr | grep fio
+#
+set -euo pipefail
+
+MODE=${1:?"Usage: $0 <baseline|sqcq-poll>"}
+DEV=${2:-/dev/sda}
+PIN_CPU=7
+RUNTIME=60
+RUNS=3
+
+TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+OUTDIR="perf-results/${MODE}_${TIMESTAMP}"
+mkdir -p "$OUTDIR"
+
+# Verify CPU pinning mapping: PIN_CPU -> hwq -> single Host vq
+echo "=== CPU/mq mapping verification ==="
+echo "PIN_CPU=$PIN_CPU"
+for d in /sys/block/$(basename $DEV)/mq/*/; do
+    dir=$(basename "$d")
+    cpus=$(cat "$d/cpu_list" 2>/dev/null || echo "?")
+    echo "  hwq $dir: CPU $cpus"
+done
+echo ""
+
+echo "=== Mode: $MODE | Device: $DEV | CPU pin: $PIN_CPU | Runs: $RUNS ==="
+echo "=== Output: $OUTDIR ==="
+echo ""
+
+# Collect Host dmesg marker (for correlating with Host-side vhost stats)
+dmesg > "$OUTDIR/guest-dmesg-before.log" 2>/dev/null || true
+
+# Test matrix: label rw bs iodepth numjobs
+TESTS=(
+    "4k-randread-od1-nj1    randread   4k  1  1"
+    "4k-randread-od32-nj1   randread   4k  32 1"
+    "4k-randread-od32-nj4   randread   4k  32 4"
+    "4k-randread-od32-nj8   randread   4k  32 8"
+    "4k-randwrite-od1-nj1   randwrite  4k  1  1"
+    "4k-randwrite-od32-nj1  randwrite  4k  32 1"
+    "4k-randwrite-od32-nj4  randwrite  4k  32 4"
+    "4k-randwrite-od32-nj8  randwrite  4k  32 8"
+    "128k-seqread-od1-nj1   read       128k 1  1"
+    "128k-seqread-od32-nj1  read       128k 32 1"
+    "128k-seqread-od32-nj4  read       128k 32 4"
+    "128k-seqread-od32-nj8  read       128k 32 8"
+    "128k-seqwrite-od1-nj1  write      128k 1  1"
+    "128k-seqwrite-od32-nj1 write      128k 32 1"
+    "128k-seqwrite-od32-nj4 write      128k 32 4"
+    "128k-seqwrite-od32-nj8 write      128k 32 8"
+)
+
+for entry in "${TESTS[@]}"; do
+    read -r LABEL RW BS OD NJ <<< "$entry"
+
+    echo "=== $LABEL (iodepth=$OD, numjobs=$NJ) ==="
+
+    BEST_IOPS=0
+    BEST_FILE=""
+
+    for run in $(seq 1 $RUNS); do
+        OUTFILE="${OUTDIR}/${LABEL}_run${run}.json"
+
+        # For numjobs=1, pin to single CPU; for numjobs>1, let fio distribute
+        PIN_ARGS=""
+        if [ "$NJ" -eq 1 ]; then
+            PIN_ARGS="--cpus_allowed=$PIN_CPU --cpus_allowed_policy=shared"
+        fi
+
+        echo "  run $run/$RUNS ..."
+        fio --ioengine=libaio \
+            --direct=1 \
+            --rw="$RW" \
+            --bs="$BS" \
+            --iodepth=$OD \
+            --numjobs=$NJ \
+            $PIN_ARGS \
+            --time_based \
+            --runtime=$RUNTIME \
+            --group_reporting \
+            --percentile_list=50,95,99,99.9 \
+            --filename="$DEV" \
+            --name="$LABEL" \
+            --output-format=json \
+            --output="$OUTFILE"
+
+        # Extract IOPS from JSON
+        IOPS=$(python3 -c "
+import json, sys
+with open('$OUTFILE') as f:
+    d = json.load(f)
+jobs = d.get('jobs', [])
+if jobs:
+    rd = jobs[0].get('read', {})
+    wr = jobs[0].get('write', {})
+    iops = rd.get('iops', 0) + wr.get('iops', 0)
+    print(f'{iops:.0f}')
+else:
+    print('0')
+" 2>/dev/null || echo "0")
+
+        echo "    IOPS: $IOPS"
+
+        # Track best run
+        if [ "$IOPS" -gt "$BEST_IOPS" ]; then
+            BEST_IOPS=$IOPS
+            BEST_FILE=$OUTFILE
+        fi
+    done
+
+    # Copy best run as the canonical result
+    if [ -n "$BEST_FILE" ]; then
+        cp "$BEST_FILE" "${OUTDIR}/${LABEL}_best.json"
+    fi
+
+    echo ""
+done
+
+# Collect dmesg after
+dmesg > "$OUTDIR/guest-dmesg-after.log" 2>/dev/null || true
+
+# Generate summary
+echo "=== Generating summary ==="
+python3 - <<'PYEOF' "$OUTDIR"
+import json, sys, os, glob
+
+outdir = sys.argv[1]
+print(f"\n{'='*80}")
+print(f"  RESULTS: {os.path.basename(outdir)}")
+print(f"{'='*80}\n")
+
+header = f"{'Test':<30} {'IOPS':>10} {'p50(us)':>10} {'p99(us)':>10} 
{'p99.9(us)':>10} {'BW(MiB/s)':>10} {'lat_avg(us)':>12}"
+print(header)
+print("-" * len(header))
+
+for best in sorted(glob.glob(os.path.join(outdir, "*_best.json"))):
+    label = os.path.basename(best).replace("_best.json", "")
+    try:
+        with open(best) as f:
+            d = json.load(f)
+        jobs = d.get("jobs", [])
+        if not jobs:
+            continue
+
+        j = jobs[0]
+        rd = j.get("read", {})
+        wr = j.get("write", {})
+        iops = rd.get("iops", 0) + wr.get("iops", 0)
+        bw = (rd.get("bw", 0) + wr.get("bw", 0)) / 1024  # KiB/s -> MiB/s
+
+        lat_ns = rd.get("lat_ns", {}) or wr.get("lat_ns", {})
+        avg_us = lat_ns.get("mean", 0) / 1000
+
+        # Percentiles from clat_ns (completion latency)
+        clat = rd.get("clat_ns", {}) or wr.get("clat_ns", {})
+        pct = clat.get("percentile", {}) if isinstance(clat, dict) else {}
+        p50 = pct.get("50.000000", 0) / 1000
+        p99 = pct.get("99.000000", 0) / 1000
+        p999 = pct.get("99.900000", 0) / 1000
+
+        print(f"{label:<30} {iops:>10,.0f} {p50:>10.1f} {p99:>10.1f} 
{p999:>10.1f} {bw:>10.1f} {avg_us:>12.1f}")
+    except Exception as e:
+        print(f"{label:<30} ERROR: {e}")
+
+print()
+PYEOF
+
+echo ""
+echo "=== Done. Results in $OUTDIR ==="
+echo "=== After running both modes, compare with: ==="
+echo "    diff <(python3 summarize.py perf-results/baseline_*) <(python3 
summarize.py perf-results/sqcq-poll_*)"
+echo ""
+echo "=== Or paste both summary tables side-by-side for comparison ==="
diff --git a/Documentation/virtio-sqcq-poll-spec.rst 
b/Documentation/virtio-sqcq-poll-spec.rst
new file mode 100644
index 0000000000000..523bb3e3a1963
--- /dev/null
+++ b/Documentation/virtio-sqcq-poll-spec.rst
@@ -0,0 +1,617 @@
+Virtio SQ/CQ Polling Mode — 技术规格书
+==========================================
+
+Host Kernel vhost 端已实现。Guest Kernel 端已实现。QEMU 端已实现。
+
+1. 概述
+-------
+
+传统 virtio 使用 MMIO kick (Guest→Device) 和 MSI-X 中断 (Device→Guest) 通知,
+每次涉及 VM exit。SQ/CQ polling 借鉴 io_uring SQPOLL 模型,在 split ring 旁引入
+SQ/CQ 门铃结构,由轮询线程检测变更替代中断,消除 VM exit。
+
+- Guest→Device:Guest 更新 SQ.idx,Device 轮询线程检测
+- Device→Guest:Device 更新 CQ.idx,Guest 轮询线程检测
+
+SQ/CQ 门铃仅做通知信号,实际数据仍通过 desc/avail/used ring 传递。
+通过 Feature Bit 协商,未协商时零开销。仅支持 split ring。
+
+1.1 地址传递架构
+~~~~~~~~~~~~~~~
+
+::
+
+  Guest Kernel          QEMU (userspace)       Host Kernel (vhost)
+  PCI config space      GPA → HVA 转换           VHOST_SET_VRING_SQCQ_ADDR
+  (写入 GPA)            ioctl (传入 HVA)            (接收 HVA)
+
+1.2 通知路径
+~~~~~~~~~~~~
+
+::
+
+  活跃路径(零开销):
+    Guest 提交 → SQ.idx → Device 轮询检测(消除 VM exit)
+    Device 完成 → CQ.idx → Guest 轮询检测(消除 VM exit)
+
+  休眠唤醒路径:
+    Guest 提交 → SQ.idx → MMIO kick → QEMU → eventfd → vhost 唤醒
+    Device 完成 → CQ.idx → eventfd_signal → QEMU → MSI-X → Guest 唤醒
+
+
+2. Feature 协商
+---------------
+
+``VIRTIO_F_SQCQ_POLL = 42``(``include/uapi/linux/virtio_config.h``)。
+
+``VIRTIO_TRANSPORT_F_END`` 从 42 更新为 43。
+该 bit 属于 transport feature range (28..43),需同时被 virtio_ring 和 virtio_pci 接受。
+vhost 端将该 feature 加入 ``VHOST_FEATURES`` 宏,在 ``VHOST_GET_FEATURES`` ioctl 中对 
Guest 可见。
+
+**协商流程:**
+
+1. Device 设置 bit 42 → Guest 接受 → finalize_features
+2. Guest 在 find_vqs 阶段分配 SQ/CQ 结构
+3. Guest 在 vp_active_vq 阶段将 SQ/CQ DMA 地址写入 PCI config space
+4. QEMU 读 GPA → 转 HVA → ``VHOST_SET_VRING_SQCQ_ADDR`` ioctl → vhost
+5. vhost 在 ``VHOST_SCSI_SET_ENDPOINT`` 时启动轮询线程
+
+任一方不支持则回退传统中断模式。
+
+
+3. 数据结构
+-----------
+
+3.1 SQ/CQ 门铃
+~~~~~~~~~~~~~~~
+
+定义于 ``include/uapi/linux/virtio_ring.h``:
+
+.. code-block:: c
+
+    struct vring_sq {
+        __virtio64 idx;             /* Producer position: Guest 更新 */
+        __u8 sq_need_wakeup;        /* 1 = Device 应被 kick 唤醒 */
+        __u8 reserved[55];
+    } __attribute__((aligned(128)));  /* 128 bytes, cache-line 对齐 */
+
+    struct vring_cq {
+        __virtio64 idx;             /* Producer position: Device 更新 */
+        __u8 cq_need_wakeup;        /* 1 = Guest 应被中断唤醒 */
+        __u8 reserved[55];
+    } __attribute__((aligned(128)));  /* 128 bytes, cache-line 对齐 */
+
+- ``sq->idx`` 对应 ``avail->idx``,``cq->idx`` 对应 ``used->idx``
+- 128 字节对齐覆盖 ARM Neoverse N2/V2、Apple M 系列等 128 字节 cache line 的 CPU,
+  消除 SQ 与 CQ 之间的 false sharing
+- ``sq/cq_need_wakeup`` 为 8-bit 直接赋值字段,用于 NEED_WAKEUP 协议(见第 6 节)
+
+3.2 PCI Config Space 寄存器
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+追加到 ``struct virtio_pci_modern_common_cfg`` 末尾(offset 62 之后),
+定义于 ``include/uapi/linux/virtio_pci.h``:
+
+.. code-block:: c
+
+    #define VIRTIO_PCI_COMMON_SQE_LO    64
+    #define VIRTIO_PCI_COMMON_SQE_HI    68
+    #define VIRTIO_PCI_COMMON_CQE_LO    72
+    #define VIRTIO_PCI_COMMON_CQE_HI    76
+
+    __le32 sqe_ring_lo;    /* offset 64: SQ DMA 地址低 32 位 */
+    __le32 sqe_ring_hi;    /* offset 68: SQ DMA 地址高 32 位 */
+    __le32 cqe_ring_lo;    /* offset 72: CQ DMA 地址低 32 位 */
+    __le32 cqe_ring_hi;    /* offset 76: CQ DMA 地址高 32 位 */
+
+受 queue_select 约束。UAPI 已定义,Guest 已实现写入,QEMU 已实现读取。
+
+3.3 vhost UAPI
+~~~~~~~~~~~~~~~
+
+.. code-block:: c
+
+    // include/uapi/linux/vhost.h
+    #define VHOST_SET_VRING_SQCQ_ADDR _IOW(VHOST_VIRTIO, 0x27, \
+                        struct vhost_vring_sqcq_addr)
+
+    // include/uapi/linux/vhost_types.h
+    struct vhost_vring_sqcq_addr {
+        unsigned int index;
+        __u64 sq_user_addr;    /* SQ doorbell HVA */
+        __u64 cq_user_addr;    /* CQ doorbell HVA */
+    };
+
+QEMU 调用时机:在 ``VHOST_SET_VRING_ADDR`` 之后、``VHOST_SCSI_SET_ENDPOINT`` 之前。
+
+3.4 写入时序
+~~~~~~~~~~~~
+
+Guest(vp_active_vq 中):queue_select → desc/avail/used 地址 → sqe/cqe_ring_lo/hi 地址
+QEMU:组合 GPA → address_space_map 转 HVA → VHOST_SET_VRING_SQCQ_ADDR ioctl
+
+
+4. 字段读写所有权
+-----------------
+
+.. list-table::
+   :header-rows: 1
+   :widths: 20 20 20 25 15
+
+   * - 字段
+     - 写入方
+     - 读取方
+     - 内存序
+     - 对应
+
+   * - sq->idx
+     - Guest
+     - Device
+     - Guest: smp_store_release; Device: get_user + smp_rmb
+     - avail->idx
+
+   * - sq->sq_need_wakeup
+     - Device
+     - Guest
+     - Device: smp_wmb + put_user + smp_mb (double-check)
+     - NEED_WAKEUP
+
+   * - cq->idx
+     - Device
+     - Guest
+     - Device: smp_wmb before put_user; Guest: smp_load_acquire
+     - used->idx
+
+   * - cq->cq_need_wakeup
+     - Guest
+     - Device
+     - Guest: smp_store_release; Device: get_user + smp_rmb
+     - NEED_WAKEUP
+
+
+5. 核心协议
+-----------
+
+5.1 Guest 提交请求 (SQ 方向)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: c
+
+    // virtqueue_kick() 中 sqcq_poll 模式
+    smp_store_release(&vring->sq->idx, avail_idx_shadow);
+    need_wakeup = smp_load_acquire(&vring->sq->sq_need_wakeup);
+    if (need_wakeup)
+        virtqueue_notify(vq);  // MMIO write,仅在 Device 休眠时
+
+**关键:** Guest 仍写 ``avail->idx``(``virtqueue_add_split()`` 完成)。
+SQ idx 是额外通知信号,让 Device 快速检测(读一个 cache line vs 读 avail ring 两次)。
+Device 的 ``handle_kick()`` 通过 ``avail->idx`` 定位 descriptor。
+
+**virtqueue_kick_prepare() 守卫:** sqcq_poll 模式下直接返回 ``true``,
+不检查 ``used->flags``(vhost 不维护该 flag)。
+
+5.2 Device 处理请求 (vhost 轮询线程)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+独立 kthread ``vhost_sqcq_poll_thread``。直接调用 ``handle_kick`` 处理提交,
+通过 ``poll_complete`` 回调处理完成,单线程闭环。
+
+主循环伪代码(drivers/vhost/vhost.c):
+
+.. code-block:: c
+
+    while (!poll_stop):
+        // 0. 处理 pending completion
+        if (atomic_read(&vq->completion_pending) && vq->poll_complete):
+            atomic_set(&vq->completion_pending, 0)
+            vq->poll_complete(vq)  // copy resp, add used, 写 cq->idx, 检查 
cq_need_wakeup
+
+        // 1. 读 SQ idx
+        ret = get_user(sq_idx_val, &vq->sq->idx)
+        smp_rmb()  // acquire
+        new_sq_idx = (u16)sq_idx_val
+
+        // 2. 有新提交:同步调用 handle_kick
+        if (new_sq_idx != vq->last_avail_idx):
+            vq->handle_kick(&vq->poll.work)
+
+        // 3. 自适应休眠(EMA 动态超时)
+        should_sleep = (last_avail_idx == last_used_idx)
+        if (!should_sleep && ema_ns > 0)
+            should_sleep = (active_io_vqs > 1) && (ema_us > 
SQCQ_SLEEP_THRESHOLD_US)
+
+        if (should_sleep):
+            timeout = ema_based_or_fallback(SQCQ_IDLE_TIMEOUT_US_MAX)
+            // 设置 NEED_WAKEUP + double-check idx
+            smp_wmb(); put_user(1, &sq->sq_need_wakeup); smp_mb()
+            if (idx changed): put_user(0, &sq->sq_need_wakeup); continue
+            // 注册 kick eventfd,带超时等待
+            kick_register(vq)
+            wait_event_interruptible_timeout(poll_wait,
+                poll_stop || kick_received || completion_pending, timeout)
+            was_kicked = READ_ONCE(kick_received)
+            kick_unregister(vq)
+            put_user(0, &sq->sq_need_wakeup)
+        else:
+            cpu_relax()
+
+5.3 I/O 完成路径
+~~~~~~~~~~~~~~~~~
+
+Poll 模式下 completion 不经过 vhost_worker:
+
+1. **TCM 完成**(中断/workqueue 上下文):``llist_add`` + 
``atomic_set(completion_pending, 1)`` + 条件 ``wake_up``
+2. **Poll thread 步骤 0**(poll thread 上下文):``poll_complete`` 回调
+3. **poll_complete 内部**:``llist_del_all`` → ``mutex_lock`` → copy response + 
``vhost_add_used`` → ``mutex_unlock`` → ``put_user(cq->idx)`` → 检查 
``cq_need_wakeup`` → ``eventfd_signal``
+
+设计要点:单线程闭环(handle_kick 和 poll_complete 顺序执行,共享 mutex 不会死锁);
+CQ idx 单写者(仅 poll_complete);条件 wake_up(仅在 poll thread 非 RUNNING 时)。
+
+5.4 vhost_enable/disable_notify 在 poll 模式下
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``vhost_enable_notify()`` → ``return false``
+- ``vhost_disable_notify()`` → ``return``
+
+通知控制由 sq_need_wakeup / cq_need_wakeup 承担,used ring flags 不再有效。
+Guest 端对称:``virtqueue_kick_prepare()`` 直接返回 true,``disable_cb/enable_cb`` 操作 
avail->flags 但 vhost 忽略。
+
+5.5 Guest 接收完成 (CQ 方向)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Guest 轮询线程(``virtio_sqcq_poll_thread``),每 device 一个线程。
+
+主循环要点:
+
+1. ``while (cq_poll_has_work())`` drain 该 vq 所有完成,callback 后 ``do_softirq()``
+2. 有工作时重置 ``last_io_ts = ktime_get()``,不 yield,快速轮询
+3. 无工作时基于 ``last_io_ts`` 时间窗口自旋(``busy_spin_us`` = 1ms)
+4. 自旋中使用 ``need_resched()`` + ``cpu_relax()`` 协作让出(io_uring SQPOLL 模型)
+5. spin 窗口过期后设置所有 vq 的 ``cq_need_wakeup`` + double-check ``more_used()``
+6. 无新工作则 ``schedule_timeout(idle_timeout_us)`` 休眠(1ms)
+7. 醒来后清除所有 vq 的 NEED_WAKEUP,不重置 spin 窗口(超时唤醒不 re-arm spin)
+
+**CQ 完成检测:** 直接使用 ``more_used(vq)`` 检查 used ring,不再轮询 ``cq->idx``。
+``cq->idx`` 仅由 Device 写入,保留用于 NEED_WAKEUP 协议的 ``cq_need_wakeup`` 字段,
+不再作为完成检测的 fast path(消除了 cq->idx 与 used ring 之间的竞态)。
+
+**CQ 访问器函数** (virtio_ring.c):
+
+- ``cq_poll_has_work(vq)``:``more_used(vq)`` — 直接检查 used ring
+- ``cq_set_need_wakeup(vq)``:``smp_store_release(&cq->cq_need_wakeup, 1)``
+- ``cq_clear_need_wakeup(vq)``:``WRITE_ONCE(cq->cq_need_wakeup, 0)``
+- ``cq_recheck(vq)``:``smp_mb()`` + ``more_used(vq)``
+
+5.6 Device 唤醒路径 (kick eventfd 桥接)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+  Guest MMIO kick → QEMU eventfd write → eventfd wq →
+  vhost_sqcq_kick_wakeup() → kick_received=true + wake_up →
+  poll thread → clear sq_need_wakeup → process sq->idx
+
+休眠前通过 ``vhost_sqcq_kick_register()`` 注册到 kick eventfd wait queue。
+关键:``vfs_poll`` 可能因 stale counter 返回 EPOLLIN,必须验证 ``sq->idx``
+确实有新工作才触发回调,否则所有 poll thread busy-loop。
+
+QEMU 端无需额外操作,现有 kick eventfd 路径即可。
+
+
+6. NEED_WAKEUP 协议(核心,必须严格遵守)
+-----------------------------------------
+
+双向对称,SQ 和 CQ 各一套。避免死锁和丢失唤醒的核心机制。
+
+6.1 SQ 方向
+~~~~~~~~~~~
+
+**Device 休眠前:**
+
+.. code-block:: pseudo
+
+    smp_wmb()
+    sq->sq_need_wakeup = 1
+    smp_mb()
+    new_sq_idx = sq->idx           // double-check
+    if (new_sq_idx != last_avail):
+        sq->sq_need_wakeup = 0     // 取消休眠
+        goto process
+    wait_event(kick_received)
+    sq->sq_need_wakeup = 0         // 醒来后清除
+
+**Guest 提交后:**
+
+.. code-block:: pseudo
+
+    smp_store_release(&sq->idx, new_idx)
+    need_wakeup = smp_load_acquire(&sq->sq_need_wakeup)
+    if (need_wakeup)
+        virtqueue_notify(vq)       // MMIO kick
+
+6.2 CQ 方向
+~~~~~~~~~~~
+
+    **Guest 休眠前:**
+
+.. code-block:: pseudo
+
+    smp_store_release(&cq->cq_need_wakeup, 1)
+    smp_mb()
+    if (more_used(vq)):             // double-check used ring
+        cq->cq_need_wakeup = 0     // 取消休眠
+        goto process
+    schedule_timeout(idle_timeout_us)
+    cq->cq_need_wakeup = 0         // 醒来后清除
+
+**Device 完成后(两阶段):**
+
+1. release_cmd:``llist_add`` + ``atomic_set(completion_pending, 1)`` + 条件 
``wake_up``
+2. poll_complete:copy resp → add used → ``put_user(cq->idx)`` → 检查 
``cq_need_wakeup`` → ``eventfd_signal``
+
+6.3 为什么需要 Double-check
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+设置 NEED_WAKEUP → 重读 idx 之间存在竞态:Guest 可能在 Device 读 idx 后、
+sleep 前提交。Device 在 sleep 前重读 idx,如果变了则取消休眠。
+此时 Guest 的 kick 可能已到也可能未到,但 Device 已在处理新工作,不影响正确性。
+
+6.4 内存序总结
+~~~~~~~~~~~~~~
+
+- idx 更新:写入方 store-release(Guest: ``smp_store_release``;Device: ``smp_wmb`` + 
``put_user``)
+- idx 读取:Guest 使用 ``more_used()`` 直接检查 used ring(不再轮询 ``cq->idx``)
+- sq_need_wakeup 读取:Guest 使用 ``smp_load_acquire``(替代 ``smp_mb`` + 
``READ_ONCE``)
+- NEED_WAKEUP 设置后 ``smp_mb()``,然后重读 used ring / sq->idx
+- NEED_WAKEUP 清除在 sleep 唤醒后立即执行
+- x86 上屏障为 no-op(TSO),ARM/RISC-V 上生成实际屏障指令
+
+
+7. QEMU 实现指南
+----------------
+
+7.1 Feature 协商
+~~~~~~~~~~~~~~~~
+
+.. code-block:: c
+
+    virtio_add_feature(&vdev->host_features, VIRTIO_F_SQCQ_POLL);
+
+7.2 GPA → HVA 转换
+~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: c
+
+    hwaddr sq_gpa = (sqe_ring_hi << 32) | sqe_ring_lo;
+    void *sq_hva = address_space_map(&address_space_memory, sq_gpa, 
sizeof(struct vring_sq), false);
+    // ... cq 类似
+    struct vhost_vring_sqcq_addr addr = { .index = idx, .sq_user_addr = 
sq_hva, .cq_user_addr = cq_hva };
+    ioctl(vhost_fd, VHOST_SET_VRING_SQCQ_ADDR, &addr);
+
+7.3 ioctl 调用顺序
+~~~~~~~~~~~~~~~~~~
+
+启动:
+
+::
+
+  1. VHOST_SET_VRING_ADDR (desc/avail/used)
+  2. VHOST_SET_VRING_SQCQ_ADDR (sq/cq) — 新增
+  3. VHOST_SET_VRING_KICK
+  4. VHOST_SET_VRING_CALL
+  5. VHOST_SCSI_SET_ENDPOINT — 触发轮询线程启动
+
+停止:
+
+::
+
+  1. VHOST_SCSI_CLEAR_ENDPOINT — 自动停止轮询线程
+  2. VHOST_SET_VRING_KICK (null_fd)
+  3. VHOST_SET_VRING_CALL (null_fd)
+
+7.4 call eventfd 处理
+~~~~~~~~~~~~~~~~~~~~~
+
+收到 eventfd signal 说明 Guest 设置了 cq_need_wakeup,需注入 MSI-X 中断。
+使用现有 ``virtio_irq()`` / ``virtio_notify_irqfd()`` 即可。
+
+即使启用 polling,Device 仍需保留中断能力(CQ NEED_WAKEUP 唤醒、config change、fallback)。
+
+7.5 内存序
+~~~~~~~~~~
+
+QEMU 用户态需使用 ``atomic_store_release`` / ``atomic_load_acquire`` 或 C11 
``stdatomic.h``。
+
+
+8. Host Kernel vhost 端实现
+----------------------------
+
+8.1 vhost_virtqueue 扩展
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: c
+
+    struct vhost_virtqueue {
+        /* ... 现有字段 ... */
+        /* SQ/CQ polling mode */
+        bool sqcq_poll;
+        struct vring_sq __user *sq;
+        struct vring_cq __user *cq;
+        struct task_struct *poll_task;
+        bool poll_stop;
+        wait_queue_head_t poll_wait;
+        struct completion poll_started, poll_stopped;
+        bool kick_received;
+        atomic_t completion_pending;
+        void (*poll_complete)(struct vhost_virtqueue *vq);
+        wait_queue_head_t *kick_wqh;
+        wait_queue_entry_t kick_wait;
+        poll_table kick_pt;
+        /* 10s 统计窗口和 EMA */
+        u64 ema_latency_ns;     /* EMA of cmd processing latency */
+        u64 ema_interval_ns;    /* EMA of cmd-to-cmd interval */
+        /* ... lat_*/stat_* 统计字段 ... */
+    };
+
+8.2 ioctl 处理
+~~~~~~~~~~~~~~
+
+``vhost_vring_set_sqcq_addr()``:copy_from_user → 验证地址/对齐 → access_ok(backend 
已配置时)→ 存储 sq/cq。
+另含自动启动逻辑:当 private_data && sq && cq && !sqcq_poll && poll_complete && feature 
时调 ``vhost_sqcq_poll_start()``。
+
+8.3 独立轮询线程
+~~~~~~~~~~~~~~~~~
+
+kthread ``vhost-sqcq-<pid>-<vq_offset>``,通过 ``kthread_use_mm(dev->mm)`` 绑定 
QEMU 地址空间。
+直接调用 ``handle_kick`` 处理提交,``poll_complete`` 处理完成,单线程闭环。
+
+API:``vhost_sqcq_poll_start(vq)`` / ``vhost_sqcq_poll_stop(vq)``。
+start 时先创建 kthread 并等待 ``poll_started`` completion,然后才设置 ``sqcq_poll = true``,
+确保 ``vhost_signal()`` 看到 sqcq_poll 时 ``poll_task`` 已有效(消除 NULL deref 窗口)。
+stop 后清除 poll_task、sqcq_poll、sq、cq,flush 残留 completion。
+
+8.4 策略常量与 CPU 绑核
+~~~~~~~~~~~~~~~~~~~~~~~
+
+策略常量(编译期固定,``#define``):
+
+.. code-block:: c
+
+    #define SQCQ_IDLE_TIMEOUT_US_MAX    1000U   /* sleep 时长上界 (us) */
+    #define SQCQ_SLEEP_EMA_MULT         2       /* sleep 时长 = N * ema_latency 
*/
+    #define SQCQ_SLEEP_THRESHOLD_US     20U     /* 快于此时长的后端选择 spin */
+    #define SQCQ_SPIN_EMA_MULT          2       /* inflight spin 窗口 = N * 
ema_latency */
+    #define SQCQ_ACTIVE_TIMEOUT_NS      1000000U    /* 无 inflight 固定自旋窗口 1ms */
+    #define SQCQ_STALE_TIMEOUT_NS       1000000000ULL /* sq_was_active 兜底清除 1s 
*/
+
+**CPU 绑核**:round-robin(``sqcq_poll_cpu_rr`` 全局原子计数器 ``% num_online_cpus()``),
+分散到不同在线 CPU。计数器取模确保重启后不会溢出到 fallback(全绑 cpu0)。
+
+**协作式自旋**(io_uring SQPOLL 模型):两条自旋路径均使用
+``need_resched()`` / ``cond_resched()`` 协作让出——自旋时若调度器标记需要让出,
+主动 ``cond_resched()``,避免饿死同核其他任务。线程保持 TASK_RUNNING(轮询
+``sq->idx``),不进入 sleep 路径、不设 ``sq_need_wakeup``,从而消除 Guest kick
+(VM exit)。仅在无 inflight 且超过 1ms 无新提交时才真正 sleep。
+
+**TCM 完成 CPU 导向**:``sqcq_poll_active_cpu_mask`` 记录忙 poll 线程占用的 CPU
+(跟着 ``sq_was_active`` 翻转)。``vhost_sqcq_pick_completion_cpu()`` 从空闲 CPU
+中选一个给 TCM 完成,避开忙 poll 核;全占满时回退到 next-CPU。
+
+8.5 vhost_signal / notify
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``vhost_signal()``:poll 模式下设 completion_pending + 条件 wake_up。SCSI completion 
不经过此路径
+- ``vhost_enable_notify()``:poll 模式下 return false
+- ``vhost_disable_notify()``:poll 模式下 return
+- ``vhost_dev_stop()``:仅对 sqcq_poll vq 加锁停止
+
+8.6 vhost-scsi 集成
+~~~~~~~~~~~~~~~~~~~
+
+``set_endpoint`` 中对 I/O VQs(index >= VHOST_SCSI_VQ_IO):如果 feature + sq + cq 则 
start。
+``clear_endpoint`` 中先 stop 再 clear backend。
+
+8.7 call_ctx.ctx NULL 检查
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``poll_complete`` 中 ``eventfd_signal(call_ctx.ctx)`` 前做 NULL 检查。
+QEMU 必须在 SET_ENDPOINT 之前完成 SET_VRING_CALL 设置。
+
+8.8 空闲策略与 EMA
+~~~~~~~~~~~~~~~~~~
+
+系统维护两个独立的 EMA:
+- ``ema_latency_ns``:cmd 处理延迟(submit → process_completions)的 EMA,每次 
``poll_complete`` 后更新 ``old - old/8 + avg/8``(溢出安全)
+- ``ema_interval_ns``:cmd-to-cmd 间隔的 EMA,每次检测到 sq->idx 变化时更新 ``old - old/8 + 
new/8``
+
+**无 inflight 自旋**(io_uring SQPOLL 模型):固定 ``SQCQ_ACTIVE_TIMEOUT_NS``(1ms)
+窗口 + ``cond_resched()`` 协作让出。线程持续轮询 ``sq->idx`` 接住下一条提交,
+不进入 sleep 路径、不设 ``sq_need_wakeup`` → 消除 Guest kick(VM exit)。
+仅在超过 1ms 无新提交时才 sleep。
+
+**有 inflight 自旋**:``spin_ns = SQCQ_SPIN_EMA_MULT * ema_latency_ns``,
+``cond_resched()`` 协作让出。超出窗口或多 VQ + 高延迟时 sleep。
+
+Sleep 动态超时:``timeout = ema_latency / (1000 / SQCQ_SLEEP_EMA_MULT)``,clamp 到 
[10us, SQCQ_IDLE_TIMEOUT_US_MAX]。
+多 VQ 休眠决策:有 inflight 时仅在 ``sqcq_active_io_vqs > 1`` 且 ``ema_latency_us > 
SQCQ_SLEEP_THRESHOLD_US`` 时休眠。
+
+8.9 统计与诊断
+~~~~~~~~~~~~~~
+
+10s 统计窗口,打印 
cq/fail/sq_hits/enters/wkick/wcomp/avg/kick_pct/wait_pct/cqw_pct/spin_pct/wake_avg/wake_n/interval/ema_lat/inflight
 到 dmesg。
+Stuck 检测:wait_event 超时或窗口内无完成但有 inflight 时告警。
+
+9. Guest Kernel 端实现
+-----------------------
+
+9.1 SQ/CQ DMA 分配
+~~~~~~~~~~~~~~~~~~
+
+``virtio_alloc_sqcq()``:``dma_alloc_coherent()`` 一次分配 256 bytes(sq+cq 各 128 
字节),
+在 ``vring_create_virtqueue_split()`` 中分配,``vring_free`` 时释放。
+
+9.2 地址传递
+~~~~~~~~~~~~
+
+``vp_modern_queue_address_sqcq()``:在 ``vp_active_vq()`` 中,通过 PCI common config
+的 queue_select + sqe/cqe_ring_lo/hi 写入 64 位 DMA 地址。
+
+9.3 提交路径
+~~~~~~~~~~~~
+
+``virtqueue_kick()``:``smp_store_release(&sq->idx, shadow)`` → 如果 NEED_WAKEUP 
则 MMIO kick。
+``virtqueue_kick_prepare()``:sqqcq_poll 下直接 return true。
+
+9.4 CQ 轮询线程
+~~~~~~~~~~~~~~~~~
+
+独立 kthread ``virtio-sqcq/<device_name>``,每 device 一个线程(``struct 
virtio_sqcq_poll_data``)。
+
+**CQ 访问器函数** (virtio_ring.c):
+
+- ``cq_poll_has_work(vq)``:``smp_load_acquire(&cq->idx) != last_cq_head``
+- ``cq_poll_consume(vq)``:snapshot cq->idx(callback 前)
+- ``cq_set_need_wakeup(vq)``:``smp_store_release(&cq->cq_need_wakeup, 1)``
+- ``cq_clear_need_wakeup(vq)``:``WRITE_ONCE(cq->cq_need_wakeup, 0)``
+- ``cq_recheck(vq)``:``smp_mb()`` + ``smp_load_acquire(&cq->idx) != 
last_cq_head``
+
+**线程生命周期:** ``vp_modern_find_vqs()`` 中启动(``virtio_start_sqcq_poll(vdev, 1000, 
1000)``),
+注册所有 poll 活跃的 vq。``vp_del_vqs()`` 中停止(``virtio_stop_sqcq_poll`` 内部释放所有 vq 
node)。
+
+**启动参数:** ``idle_timeout_us=1000``(1ms sleep 超时),``busy_spin_us=1000``(1ms 
spin 预算)。
+不绑核(调度器自由分配),无指数退避(固定 1ms sleep)。
+
+**config 空间大小检查:** ``vp_check_common_size()`` 验证 common config 至少 80 字节。
+
+
+10. 与现有机制的关系
+---------------------
+
+- **EVENT_IDX:** 可协商但对 polling vq 无效,被 sq/cq_need_wakeup 替代
+- **virtio-net NAPI:** 可共存,NAPI 可直接检查 CQ
+- **blk-mq iopoll:** 互补(iopoll 同步快路径,SQ/CQ 替代异步中断路径)
+- **Live Migration:** reset 时回退中断模式,迁移后 Guest 重新协商
+
+
+11. 文件清单
+------------
+
+**Guest Kernel:** ``virtio_config.h`` (feature bit), ``virtio_ring.h`` 
(vring_sq/cq),
+``virtio_pci.h`` (PCI offsets), ``virtio_ring.c`` (分配/SQ提交/CQ accessor),
+``virtio_sqcq_poll.c`` (CQ 轮询线程), ``virtio_pci_modern.c`` 
(feature/queue激活/线程生命周期),
+``virtio_pci_common.c`` (vp_del_vqs), ``virtio_pci_modern_dev.c`` (PCI config 
映射)
+
+**Host Kernel vhost:** ``vhost.h`` (ioctl), ``vhost_types.h`` (sqcq_addr 
struct),
+``vhost.h/vhost.c`` (轮询线程/ioctl/signal/生命周期), ``scsi.c`` (vhost-scsi 集成)
+
+**QEMU:** ``virtio-pci.c`` (feature/config), ``virtio-pci-modern.c`` 
(BAR/GPA→HVA)
+
+
+12. 验证方案
+------------
+
+**功能:** QEMU 带 VIRTIO_F_SQCQ_POLL + virtio-scsi → Guest 读写验证正确性。
+
+**性能:** fio 对比中断模式 vs polling,观察 IOPS/延迟/VM exit 次数。
+
+**正确性:** 24h+ stress test;idle/wakeup 验证;NEED_WAKEUP 边界条件;ARM/RISC-V 内存序验证。
diff --git a/Documentation/virtio-sqcq-poll-virtio-spec.rst 
b/Documentation/virtio-sqcq-poll-virtio-spec.rst
new file mode 100644
index 0000000000000..26ae41860a8e5
--- /dev/null
+++ b/Documentation/virtio-sqcq-poll-virtio-spec.rst
@@ -0,0 +1,355 @@
+SQ/CQ Doorbell Polling
+======================
+
+This section describes the SQ/CQ doorbell polling mechanism
+(``VIRTIO_F_SQCQ_POLL``), which allows the driver and device to use
+shared-memory doorbells instead of MMIO writes (notifications) and
+interrupts, eliminating VM exits on both submission and completion paths.
+
+This feature is only supported with split virtqueues. A driver or
+device that negotiates this feature MUST NOT also negotiate
+``VIRTIO_F_RING_PACKED``.
+
+
+Feature bit
+-----------
+
+``VIRTIO_F_SQCQ_POLL (42)`` — SQ/CQ Doorbell Polling.
+
+This feature is in the transport feature range (28..43). Both
+``virtio_ring`` and ``virtio_pci`` MUST accept this feature for it
+to be negotiated.
+
+
+Data Structures
+---------------
+
+SQ Doorbell
+~~~~~~~~~~~
+
+The SQ (Submission Queue) doorbell is a cache-line aligned structure
+written by the driver and read by the device:
+
+.. code-block:: c
+
+    struct vring_sq {
+        le64 idx;
+        u8   sq_need_wakeup;
+        u8   reserved[55];
+    };
+
+The structure MUST be aligned to the cache line size of the platform
+(minimum 64 bytes; 128 bytes recommended for platforms with 128-byte
+cache lines such as ARM Neoverse N2/V2).
+
+``idx`` corresponds to ``avail->idx`` and is written by the driver
+using ``smp_store_release()``.
+
+``sq_need_wakeup`` is a single-byte flag written by the device,
+indicating whether the device's poll thread is sleeping and needs to
+be woken by an MMIO write (kick).
+
+CQ Doorbell
+~~~~~~~~~~~
+
+The CQ (Completion Queue) doorbell is a cache-line aligned structure
+written by the device and read by the driver:
+
+.. code-block:: c
+
+    struct vring_cq {
+        le64 idx;
+        u8   cq_need_wakeup;
+        u8   reserved[55];
+    };
+
+The structure MUST be aligned to the same alignment as ``vring_sq``.
+
+``idx`` corresponds to ``used->idx`` and is written by the device.
+
+``cq_need_wakeup`` is a single-byte flag written by the driver,
+indicating whether the driver's poll thread is sleeping and needs to
+be woken by an interrupt.
+
+
+PCI Configuration
+-----------------
+
+The SQ and CQ doorbell DMA addresses are passed through the PCI
+common configuration space. Four new registers are appended to
+``struct virtio_pci_modern_common_cfg``:
+
+==============  ========  ========================================
+Offset          Field     Description
+==============  ========  ========================================
+64              sqe_ring_lo  SQ doorbell DMA address, low 32 bits
+68              sqe_ring_hi  SQ doorbell DMA address, high 32 bits
+72              cqe_ring_lo  CQ doorbell DMA address, low 32 bits
+76              cqe_ring_hi  CQ doorbell DMA address, high 32 bits
+==============  ========  ========================================
+
+These registers are scoped by ``queue_select``. The driver writes
+the SQ and CQ doorbell DMA addresses for each virtqueue during
+``vp_active_vq``.
+
+The device (QEMU) translates the guest physical address to a host
+virtual address and passes it to the vhost backend via
+``VHOST_SET_VRING_SQCQ_ADDR`` ioctl.
+
+
+Allocation
+----------
+
+The driver allocates SQ and CQ doorbells as a single
+``dma_alloc_coherent()`` call (contiguous physical memory):
+
+.. code-block:: c
+
+    size_t total = sizeof(struct vring_sq) + sizeof(struct vring_cq);
+    void *buf = dma_alloc_coherent(dev, total, &dma_addr, GFP_KERNEL);
+    vq->sq = buf;
+    vq->cq = buf + sizeof(struct vring_sq);
+
+Control and event virtqueues MUST NOT use SQ/CQ polling. Only request
+virtqueues (index >= VIRTIO_SCSI_VQ_BASE for virtio-scsi) negotiate
+this feature.
+
+
+Submission Path (Driver → Device)
+---------------------------------
+
+When the driver has new requests available:
+
+1. The driver writes descriptors to the avail ring as usual
+2. The driver updates ``sq->idx`` using ``smp_store_release()``
+3. The driver reads ``sq_need_wakeup`` using ``smp_load_acquire()``
+
+If ``sq_need_wakeup`` is 0, the device's poll thread is actively
+spinning and will detect the new ``sq->idx`` value without any
+further action.
+
+If ``sq_need_wakeup`` is 1, the device's poll thread is sleeping.
+The driver MUST perform a traditional MMIO write (kick) to wake it.
+
+.. code-block:: pseudo
+
+    smp_store_release(&sq->idx, avail_idx_shadow)
+    need_wakeup = smp_load_acquire(&sq->sq_need_wakeup)
+    if (need_wakeup):
+        mmio_kick(vq)
+
+The ``virtqueue_kick_prepare()`` function MUST return ``true``
+unconditionally when SQ/CQ polling is active, bypassing the
+``used->flags`` check.
+
+
+Completion Path (Device → Driver)
+---------------------------------
+
+Device Side
+~~~~~~~~~~~
+
+When the device processes completions:
+
+1. The device writes completion entries to the used ring as usual
+2. The device's poll thread writes ``cq->idx`` to signal completion
+
+The device's poll thread is a dedicated kernel thread that processes
+both submissions (via ``handle_kick``) and completions (via
+``poll_complete`` callback) in a single-threaded loop.
+
+Driver Side
+~~~~~~~~~~~
+
+The driver's poll thread (one per device) detects completions by
+checking ``more_used()`` — directly comparing ``used->idx`` with
+``last_used_idx``:
+
+.. code-block:: pseudo
+
+    while more_used(vq):
+        consume_and_callback(vq)
+        do_softirq()
+
+Note: The driver polls ``used->idx`` directly rather than ``cq->idx``,
+because ``cq->idx`` may lag behind ``used->idx`` (the device writes
+the used ring entries before updating ``cq->idx``). Polling
+``used->idx`` ensures the lowest-latency completion detection.
+
+``cq->idx`` is retained for the ``cq_need_wakeup`` sleep/wake
+protocol (see below) but is not polled as a fast-path completion
+detector.
+
+
+NEED_WAKEUP Protocol
+--------------------
+
+The NEED_WAKEUP protocol prevents lost wakeups when either side
+transitions from polling to sleeping. It is symmetric: SQ uses
+``sq_need_wakeup`` and CQ uses ``cq_need_wakeup``.
+
+SQ NEED_WAKEUP (Device sleeping)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before the device's poll thread sleeps:
+
+1. Set ``sq_need_wakeup = 1`` (via ``put_user``)
+2. Execute ``smp_mb()``
+3. Re-read ``sq->idx`` — if it changed, cancel sleep
+
+After waking (via eventfd or timeout):
+- Clear ``sq_need_wakeup = 0``
+
+The driver checks ``sq_need_wakeup`` after writing ``sq->idx`` and
+performs an MMIO kick only if the flag is set.
+
+CQ NEED_WAKEUP (Driver sleeping)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before the driver's poll thread sleeps:
+
+1. Set ``cq_need_wakeup = 1`` (via ``smp_store_release``)
+2. Execute ``smp_mb()``
+3. Re-check ``more_used()`` — if work found, cancel sleep
+
+After waking (via eventfd/interrupt or timeout):
+- Clear ``cq_need_wakeup = 0``
+
+The device checks ``cq_need_wakeup`` after writing ``cq->idx`` and
+sends an eventfd signal only if the flag is set.
+
+
+Memory Ordering
+---------------
+
+The following memory ordering rules MUST be observed:
+
+====================  ============================  
============================
+Field                 Writer                       Reader
+====================  ============================  
============================
+``sq->idx``           ``smp_store_release``         ``get_user`` + ``smp_rmb``
+``sq_need_wakeup``    ``put_user`` + ``smp_wmb``    ``smp_load_acquire``
+``cq->idx``           ``put_user`` (with ``smp_wmb``) Not polled (see note)
+``cq_need_wakeup``    ``smp_store_release``         ``get_user`` + ``smp_rmb``
+``used->idx``         Device (via ``vhost_add_used``) ``more_used()`` (plain 
read)
+====================  ============================  
============================
+
+Note: ``cq->idx`` is written by the device but is not polled by the
+driver's fast path. The driver uses ``more_used()`` (checking
+``used->idx``) for completion detection, which is the authoritative
+source of completions.
+
+On x86 (TSO memory model), all barriers are no-ops. On ARM64 and
+RISC-V, ``smp_store_release`` generates ``stlr`` and
+``smp_load_acquire`` generates ``ldar``, which are lighter than full
+memory barriers (``dmb ish``).
+
+
+Poll Thread Behavior
+--------------------
+
+Device Poll Thread (vhost)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One kernel thread per virtqueue. Each thread:
+
+1. Polls ``sq->idx`` for new submissions and ``completion_pending``
+   for completions
+2. When active: busy-spins using ``need_resched()`` + ``cpu_relax()``
+   for cooperative yielding (io_uring SQPOLL pattern)
+3. Spin budget: fixed 1ms (``SQCQ_ACTIVE_TIMEOUT_NS``) for no-inflight,
+   or ``2 × ema_latency`` for has-inflight
+4. After spin budget expires: sets ``sq_need_wakeup``, sleeps via
+   ``wait_event_interruptible_timeout``
+5. Sleep timeout: adaptive, based on EMA latency, clamped to [10us, 1ms]
+
+CPU affinity: round-robin across online CPUs
+(``atomic_counter % num_online_cpus()``).
+
+Driver Poll Thread
+~~~~~~~~~~~~~~~~~~
+
+One kernel thread per device (serves all registered virtqueues). The
+thread:
+
+1. Iterates all registered virtqueues, polls ``more_used()`` for each
+2. When active: busy-spins within a time budget (``busy_spin_us = 1ms``)
+   from ``last_io_ts``, using ``need_resched()`` + ``cpu_relax()``
+3. After spin budget expires: sets ``cq_need_wakeup`` on all VQs,
+   double-checks ``more_used()``, sleeps via ``schedule_timeout``
+4. Sleep timeout: fixed 1ms (``idle_timeout_us = 1000``)
+5. ``do_softirq()`` called after each completion callback (no hardware
+   interrupts means softirqs must be explicitly flushed)
+
+Parameters (configurable at thread creation):
+
+- ``idle_timeout_us``: sleep timeout in microseconds (default: 1000)
+- ``busy_spin_us``: spin budget in microseconds (default: 1000)
+
+
+Feature Negotiation
+-------------------
+
+1. The device offers ``VIRTIO_F_SQCQ_POLL`` in its feature bits
+2. The driver accepts the feature during ``finalize_features``
+3. The driver allocates SQ/CQ doorbells in ``find_vqs``
+4. The driver writes SQ/CQ DMA addresses to PCI config space
+5. The device (vhost) receives addresses and starts poll threads
+6. ``virtio_features_ok()`` MUST reject
+   ``VIRTIO_F_SQCQ_POLL | VIRTIO_F_RING_PACKED``
+
+If either side does not support the feature, traditional MMIO kick +
+MSI-X interrupt mode is used with zero overhead.
+
+
+Mutual Exclusion
+----------------
+
+``VIRTIO_F_SQCQ_POLL`` and ``VIRTIO_F_RING_PACKED`` MUST NOT be
+negotiated simultaneously. The driver's ``virtio_features_ok()``
+function rejects this combination.
+
+This restriction exists because the SQ/CQ doorbell implementation
+accesses split-ring-specific fields (``avail_idx_shadow``,
+``vring->used->idx``) unconditionally in ``virtqueue_notify()``.
+Packed ring support is a future enhancement.
+
+
+Relationship to Existing Mechanisms
+-----------------------------------
+
+- **EVENT_IDX** (``VIRTIO_RING_F_EVENT_IDX``): may be negotiated but
+  has no effect on polling virtqueues. Notification suppression is
+  handled by ``sq_need_wakeup`` / ``cq_need_wakeup`` instead.
+
+- **blk-mq iopoll**: complementary. iopoll handles synchronous
+  polling from the submitting task; SQ/CQ replaces the asynchronous
+  interrupt path.
+
+- **vhost-vDPA**: vDPA provides its own doorbell mmap mechanism.
+  SQ/CQ polling is specific to the vhost-scsi kernel backend and
+  does not require vDPA hardware support.
+
+
+Performance Characteristics
+---------------------------
+
+Benchmark results (fio, 4K random I/O, arm64 with 8 vCPUs):
+
+============================  ============  ===========  =========
+Test                          Baseline      SQ/CQ Poll   Change
+============================  ============  ===========  =========
+randread od1 nj1              22,427 IOPS   28,289 IOPS  +26.1%
+randread od32 nj1             89,910 IOPS   75,665 IOPS  -15.8%
+randread od32 nj4             186,763 IOPS  379,549 IOPS +103.2%
+randread od32 nj8             199,967 IOPS  550,633 IOPS +175.4%
+randwrite od1 nj1             21,912 IOPS   27,261 IOPS  +24.4%
+randwrite od32 nj1            85,349 IOPS   81,389 IOPS   -4.6%
+randwrite od32 nj4            190,443 IOPS  355,811 IOPS  +86.8%
+randwrite od32 nj8            196,552 IOPS  566,640 IOPS +188.3%
+============================  ============  ===========  =========
+
+Multi-queue workloads (nj4/nj8) see 87-188% improvement from
+eliminated VM exits on both submission and completion paths.
+Single-queue high-queue-depth workloads (od32 nj1) show a
+minor regression due to polling overhead vs. the VM exit savings.
diff --git a/build-riscv-kernel.sh b/build-riscv-kernel.sh
new file mode 100644
index 0000000000000..2ace46278369e
--- /dev/null
+++ b/build-riscv-kernel.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# 设置变量
+export ARCH=riscv
+export CROSS_COMPILE=riscv64-linux-gnu-
+export KERNEL_DIR=$(pwd)
+export OUTPUT_DIR="${KERNEL_DIR}/output"
+
+# 清理
+make mrproper
+
+# 配置
+make defconfig
+# 或使用特定配置
+# cp ${CONFIG_FILE} .config
+
+# 编译内核
+echo "开始编译内核..."
+make -j$(nproc) || exit 1
+
+# 编译设备树
+echo "编译设备树..."
+make dtbs || exit 1
+
+echo "编译完成!"
+echo "内核镜像: ${KERNEL_DIR}/arch/riscv/boot/Image"
diff --git a/drivers/vhost/scsi.c b/drivers/vhost/scsi.c
index 1c22880e72267..4d65205fcb23e 100644
--- a/drivers/vhost/scsi.c
+++ b/drivers/vhost/scsi.c
@@ -142,6 +142,7 @@ struct vhost_scsi_cmd {
        struct llist_node tvc_completion_list;
        /* Used to track inflight cmd */
        struct vhost_scsi_inflight *inflight;
+       ktime_t tvc_submit_ts;
 };
 
 struct vhost_scsi_nexus {
@@ -234,6 +235,7 @@ struct vhost_scsi_virtqueue {
 
        struct vhost_work completion_work;
        struct llist_head completion_list;
+       ktime_t stats_window;
 };
 
 struct vhost_scsi {
@@ -490,9 +492,18 @@ static void vhost_scsi_release_cmd(struct se_cmd *se_cmd)
                struct vhost_scsi_virtqueue *svq =  container_of(cmd->tvc_vq,
                                        struct vhost_scsi_virtqueue, vq);
 
-               llist_add(&cmd->tvc_completion_list, &svq->completion_list);
-               if (!vhost_vq_work_queue(&svq->vq, &svq->completion_work))
-                       vhost_scsi_drop_cmds(svq);
+                       llist_add(&cmd->tvc_completion_list, 
&svq->completion_list);
+                       if (READ_ONCE(svq->vq.sqcq_poll)) {
+                               smp_wmb();
+                               atomic_set(&svq->vq.completion_pending, 1);
+                               smp_mb();
+                               if (!READ_ONCE(svq->vq.poll_stop) &&
+                                   READ_ONCE(svq->vq.poll_task) &&
+                                   !task_is_running(svq->vq.poll_task))
+                                       wake_up(&svq->vq.poll_wait);
+                       } else if (!vhost_vq_work_queue(&svq->vq, 
&svq->completion_work)) {
+                               vhost_scsi_drop_cmds(svq);
+                       }
        }
 }
 
@@ -678,15 +689,11 @@ static int vhost_scsi_copy_sgl_to_iov(struct 
vhost_scsi_cmd *cmd)
        return 0;
 }
 
-/* Fill in status and signal that we are done processing this command
- *
- * This is scheduled in the vhost work queue so we are called with the owner
- * process mm and can access the vring.
+/* Process completion_list: copy responses, add used entries, release cmds.
+ * Returns true if any completions were signalled.
  */
-static void vhost_scsi_complete_cmd_work(struct vhost_work *work)
+static bool vhost_scsi_process_completions(struct vhost_scsi_virtqueue *svq)
 {
-       struct vhost_scsi_virtqueue *svq = container_of(work,
-                               struct vhost_scsi_virtqueue, completion_work);
        struct virtio_scsi_cmd_resp v_rsp;
        struct vhost_scsi_cmd *cmd, *t;
        struct llist_node *llnode;
@@ -726,6 +733,9 @@ static void vhost_scsi_complete_cmd_work(struct vhost_work 
*work)
                        signal = true;
 
                        vhost_add_used(cmd->tvc_vq, cmd->tvc_vq_desc, 0);
+                       cmd->tvc_vq->lat_scsi_ns +=
+                               ktime_to_ns(ktime_sub(ktime_get(), 
cmd->tvc_submit_ts));
+                       cmd->tvc_vq->stat_completions++;
                } else
                        pr_err("Faulted on virtio_scsi_cmd_resp\n");
 
@@ -737,10 +747,70 @@ static void vhost_scsi_complete_cmd_work(struct 
vhost_work *work)
 
        mutex_unlock(&svq->vq.mutex);
 
+       return signal;
+}
+
+/* Worker-path completion (non-poll mode only) */
+static void vhost_scsi_complete_cmd_work(struct vhost_work *work)
+{
+       struct vhost_scsi_virtqueue *svq = container_of(work,
+                               struct vhost_scsi_virtqueue, completion_work);
+       struct vhost_virtqueue *vq = &svq->vq;
+       bool signal;
+
+       signal = vhost_scsi_process_completions(svq);
+
        if (signal)
                vhost_signal(&svq->vs->dev, &svq->vq);
+
+       if (vq->stat_completions > 0) {
+               u64 now = ktime_get_ns();
+
+               if (!svq->stats_window)
+                       svq->stats_window = ns_to_ktime(now);
+
+               if (now - ktime_to_ns(svq->stats_window) >= 10 * NSEC_PER_SEC) {
+                       int vi;
+
+                       for (vi = 0; vi < svq->vs->dev.nvqs; vi++)
+                               if (svq->vs->dev.vqs[vi] == vq)
+                                       break;
+                       pr_info("vhost-baseline: vq[%d] cmd=%llu 
avg_lat=%lluns\n",
+                              vi, vq->stat_completions,
+                              div64_u64(vq->lat_scsi_ns, 
vq->stat_completions));
+                       svq->stats_window = ns_to_ktime(now);
+                       vq->lat_scsi_ns = 0;
+                       vq->stat_completions = 0;
+               }
+       }
 }
 
+/* Poll-thread-path completion: write cq->idx and signal Guest directly */
+static void vhost_scsi_poll_complete(struct vhost_virtqueue *vq)
+{
+       struct vhost_scsi_virtqueue *svq = container_of(vq,
+                               struct vhost_scsi_virtqueue, vq);
+       bool signal;
+
+       signal = vhost_scsi_process_completions(svq);
+
+       if (signal && svq->vq.cq) {
+               __virtio64 cq_idx_val;
+               __u8 cq_need_wakeup;
+
+               smp_mb();
+               cq_idx_val = (__force 
__virtio64)READ_ONCE(svq->vq.last_used_idx);
+               put_user(cq_idx_val, &svq->vq.cq->idx);
+               smp_mb(); /* Ensure cq->idx is visible before reading 
cq_need_wakeup */
+               if (!get_user(cq_need_wakeup, &svq->vq.cq->cq_need_wakeup)) {
+                       smp_rmb();
+                       if (cq_need_wakeup && svq->vq.call_ctx.ctx)
+                               eventfd_signal(svq->vq.call_ctx.ctx);
+               }
+       }
+}
+
+
 static struct vhost_scsi_cmd *
 vhost_scsi_get_cmd(struct vhost_virtqueue *vq, u64 scsi_tag)
 {
@@ -1060,9 +1130,25 @@ static void vhost_scsi_target_queue_cmd(struct 
vhost_scsi_nexus *nexus,
        }
 
        se_cmd->tag = 0;
+       cmd->tvc_submit_ts = ktime_get();
        target_init_cmd(se_cmd, nexus->tvn_se_sess, &cmd->tvc_sense_buf[0],
                        lun, exp_data_len, vhost_scsi_to_tcm_attr(task_attr),
-                       data_dir, TARGET_SCF_ACK_KREF);
+                       data_dir,
+                       TARGET_SCF_ACK_KREF |
+                       (READ_ONCE(cmd->tvc_vq->sqcq_poll) ?
+                                       TARGET_SCF_USE_CPUID : 0));
+
+       /* Poll mode: steer TCM completion to next CPU to avoid
+        * starvation behind poll thread. Skip if fabric has
+        * custom cmd_compl_affinity (-2=UNBOUND or specific CPU).
+        */
+       if (READ_ONCE(cmd->tvc_vq->sqcq_poll) && num_online_cpus() > 1) {
+               struct se_wwn *wwn = se_cmd->se_sess->se_tpg->se_tpg_wwn;
+
+               if (!wwn || wwn->cmd_compl_affinity == SE_COMPL_AFFINITY_CPUID)
+                       se_cmd->cpuid =
+                               
vhost_sqcq_pick_completion_cpu(smp_processor_id());
+       }
 
        if (target_submit_prep(se_cmd, cdb, sg_ptr,
                               cmd->tvc_sgl_count, NULL, 0, sg_prot_ptr,
@@ -1346,7 +1432,6 @@ vhost_scsi_handle_vq(struct vhost_scsi *vs, struct 
vhost_virtqueue *vq)
                ret = vhost_scsi_chk_size(vq, &vc);
                if (ret)
                        goto err;
-
                ret = vhost_scsi_get_req(vq, &vc, &tpg);
                if (ret)
                        goto err;
@@ -2070,6 +2155,15 @@ vhost_scsi_set_endpoint(struct vhost_scsi *vs,
                        mutex_lock(&vq->mutex);
                        vhost_vq_set_backend(vq, vs_tpg);
                        vhost_vq_init_access(vq);
+                       if (i >= VHOST_SCSI_VQ_IO &&
+                           vhost_has_feature(vq, VIRTIO_F_SQCQ_POLL) &&
+                           vq->sq && vq->cq) {
+                               ret = vhost_sqcq_poll_start(vq);
+                               if (ret) {
+                                       mutex_unlock(&vq->mutex);
+                                       goto stop_poll_threads;
+                               }
+                       }
                        mutex_unlock(&vq->mutex);
                }
                ret = 0;
@@ -2086,6 +2180,19 @@ vhost_scsi_set_endpoint(struct vhost_scsi *vs,
        vs->vs_tpg = vs_tpg;
        goto out;
 
+stop_poll_threads:
+       /* VQ at index i failed to start. Stop poll threads for all
+        * previously started VQs (indices i-1 down to VHOST_SCSI_VQ_IO).
+        */
+       for (i = i - 1; i >= VHOST_SCSI_VQ_IO; i--) {
+               vq = &vs->vqs[i].vq;
+               mutex_lock(&vq->mutex);
+               vhost_sqcq_poll_stop(vq);
+               vhost_vq_set_backend(vq, NULL);
+               mutex_unlock(&vq->mutex);
+       }
+       /* Reset i for full vq cmds cleanup */
+       i = vs->dev.nvqs;
 destroy_vq_cmds:
        for (i--; i >= VHOST_SCSI_VQ_IO; i--) {
                if (!vhost_vq_get_backend(&vs->vqs[i].vq))
@@ -2163,9 +2270,17 @@ vhost_scsi_clear_endpoint(struct vhost_scsi *vs,
        /* Prevent new cmds from starting and accessing the tpgs/sessions */
        for (i = 0; i < vs->dev.nvqs; i++) {
                vq = &vs->vqs[i].vq;
-               mutex_lock(&vq->mutex);
-               vhost_vq_set_backend(vq, NULL);
-               mutex_unlock(&vq->mutex);
+               if (!READ_ONCE(vq->sqcq_poll)) {
+                       mutex_lock(&vq->mutex);
+                       vhost_vq_set_backend(vq, NULL);
+                       mutex_unlock(&vq->mutex);
+               } else {
+                       mutex_lock(&vq->mutex);
+                       vhost_sqcq_poll_stop(vq);
+                       vhost_vq_set_backend(vq, NULL);
+                       mutex_unlock(&vq->mutex);
+                       vhost_sqcq_poll_flush(vq);
+               }
        }
        /* Make sure cmds are not running before tearing them down. */
        vhost_scsi_flush(vs);
@@ -2221,6 +2336,7 @@ static int vhost_scsi_set_features(struct vhost_scsi *vs, 
u64 features)
        bool is_log, was_log;
        int i;
 
+
        if (features & ~VHOST_SCSI_FEATURES)
                return -EOPNOTSUPP;
 
@@ -2321,6 +2437,7 @@ static int vhost_scsi_open(struct inode *inode, struct 
file *f)
                vhost_work_init(&svq->completion_work,
                                vhost_scsi_complete_cmd_work);
                svq->vq.handle_kick = vhost_scsi_handle_kick;
+               svq->vq.poll_complete = vhost_scsi_poll_complete;
        }
        vhost_dev_init(&vs->dev, vqs, nvqs, UIO_MAXIOV,
                       VHOST_SCSI_WEIGHT, 0, true, NULL);
@@ -2415,6 +2532,45 @@ vhost_scsi_ioctl(struct file *f,
                if (copy_from_user(&features, featurep, sizeof features))
                        return -EFAULT;
                return vhost_scsi_set_features(vs, features);
+       case VHOST_GET_FEATURES_ARRAY: {
+               u64 count, copied;
+               u64 farr[VIRTIO_FEATURES_U64S] = {};
+
+               if (get_user(count, (u64 __user *)argp))
+                       return -EFAULT;
+               copied = min_t(u64, count, VIRTIO_FEATURES_U64S);
+               farr[0] = VHOST_SCSI_FEATURES;
+               if (copy_to_user((u64 __user *)(argp + sizeof(u64)),
+                                farr, copied * sizeof(u64)))
+                       return -EFAULT;
+               if (count > copied &&
+                   clear_user((u64 __user *)(argp + sizeof(u64) +
+                                            copied * sizeof(u64)),
+                              (count - copied) * sizeof(u64)))
+                       return -EFAULT;
+               return 0;
+       }
+       case VHOST_SET_FEATURES_ARRAY: {
+               u64 count, copied, all_features[VIRTIO_FEATURES_U64S] = {};
+               int i;
+
+               if (get_user(count, (u64 __user *)argp))
+                       return -EFAULT;
+               copied = min_t(u64, count, VIRTIO_FEATURES_U64S);
+               if (copy_from_user(all_features,
+                                  (u64 __user *)(argp + sizeof(u64)),
+                                  copied * sizeof(u64)))
+                       return -EFAULT;
+               for (i = copied; i < count; i++) {
+                       u64 tmp;
+                       if (get_user(tmp, (u64 __user *)(argp +
+                                       sizeof(u64) + i * sizeof(u64))))
+                               return -EFAULT;
+                       if (tmp)
+                               return -EOPNOTSUPP;
+               }
+               return vhost_scsi_set_features(vs, all_features[0]);
+       }
        case VHOST_NEW_WORKER:
        case VHOST_FREE_WORKER:
        case VHOST_ATTACH_VRING_WORKER:
@@ -2424,6 +2580,7 @@ vhost_scsi_ioctl(struct file *f,
                mutex_unlock(&vs->dev.mutex);
                return r;
        default:
+
                mutex_lock(&vs->dev.mutex);
                r = vhost_dev_ioctl(&vs->dev, ioctl, argp);
                /* TODO: flush backend after dev ioctl. */
diff --git a/drivers/vhost/vhost.c b/drivers/vhost/vhost.c
index 2f2c45d208832..68fb53c40b2e6 100644
--- a/drivers/vhost/vhost.c
+++ b/drivers/vhost/vhost.c
@@ -42,6 +42,20 @@ static int max_iotlb_entries = 2048;
 module_param(max_iotlb_entries, int, 0444);
 MODULE_PARM_DESC(max_iotlb_entries,
        "Maximum number of iotlb entries. (default: 2048)");
+#define SQCQ_IDLE_TIMEOUT_US_MAX       1000U
+#define SQCQ_SLEEP_EMA_MULT            2
+#define SQCQ_SPIN_EMA_MULT             2
+#define SQCQ_ACTIVE_TIMEOUT_NS         1000000U        /* cap active-VQ clear 
at 1 ms */
+#define SQCQ_STALE_TIMEOUT_NS          1000000000ULL   /* backstop EMA/active 
clear at 1 s */
+
+static atomic_t sqcq_poll_cpu_rr = ATOMIC_INIT(-1);
+static atomic_t sqcq_active_io_vqs = ATOMIC_INIT(0);
+/* CPUs with a BUSY poll thread (actively processing cmds); TCM completion
+ * steers away.  Idle (sleeping) poll threads don't occupy their CPU, so
+ * their bit is cleared via the sq_was_active transitions in the poll loop.
+ */
+static cpumask_t sqcq_poll_active_cpu_mask;
+
 static bool fork_from_owner_default = VHOST_FORK_OWNER_TASK;
 
 #ifdef CONFIG_VHOST_ENABLE_FORK_OWNER_CONTROL
@@ -390,6 +404,20 @@ static void vhost_vq_reset(struct vhost_dev *dev,
        vhost_disable_cross_endian(vq);
        vhost_reset_is_le(vq);
        vq->busyloop_timeout = 0;
+       WRITE_ONCE(vq->sqcq_poll, false);
+       vq->sq = NULL;
+       vq->cq = NULL;
+       vq->poll_task = NULL;
+       WRITE_ONCE(vq->poll_stop, false);
+       init_waitqueue_head(&vq->poll_wait);
+       init_completion(&vq->poll_started);
+       init_completion(&vq->poll_stopped);
+       WRITE_ONCE(vq->kick_received, false);
+       atomic_set(&vq->completion_pending, 0);
+       vq->kick_wqh = NULL;
+       vq->stat_completions = 0;
+       vq->lat_scsi_ns = 0;
+       vq->poll_cpu = -1;
        vq->umem = NULL;
        vq->iotlb = NULL;
        rcu_assign_pointer(vq->worker, NULL);
@@ -1168,6 +1196,15 @@ void vhost_dev_stop(struct vhost_dev *dev)
 {
        int i;
 
+       for (i = 0; i < dev->nvqs; ++i) {
+               if (!READ_ONCE(dev->vqs[i]->sqcq_poll))
+                       continue;
+               mutex_lock(&dev->vqs[i]->mutex);
+               vhost_sqcq_poll_stop(dev->vqs[i]);
+               mutex_unlock(&dev->vqs[i]->mutex);
+               vhost_sqcq_poll_flush(dev->vqs[i]);
+       }
+
        for (i = 0; i < dev->nvqs; ++i) {
                if (dev->vqs[i]->kick && dev->vqs[i]->handle_kick)
                        vhost_poll_stop(&dev->vqs[i]->poll);
@@ -2051,6 +2088,535 @@ static long vhost_vring_set_num(struct vhost_dev *d,
        return 0;
 }
 
+static int vhost_sqcq_kick_wakeup(wait_queue_entry_t *wait, unsigned int mode,
+                                  int sync, void *key)
+{
+       struct vhost_virtqueue *vq = container_of(wait,
+               struct vhost_virtqueue, kick_wait);
+
+       if (!(key_to_poll(key) & EPOLLIN))
+               return 0;
+
+       WRITE_ONCE(vq->kick_received, true);
+       wake_up(&vq->poll_wait);
+       return 0;
+}
+
+static void vhost_sqcq_kick_ptable_func(struct file *file,
+                                        wait_queue_head_t *wqh,
+                                        poll_table *pt)
+{
+       struct vhost_virtqueue *vq = container_of(pt,
+               struct vhost_virtqueue, kick_pt);
+
+       vq->kick_wqh = wqh;
+       add_wait_queue(wqh, &vq->kick_wait);
+}
+
+static void vhost_sqcq_kick_register(struct vhost_virtqueue *vq)
+{
+       init_waitqueue_func_entry(&vq->kick_wait, vhost_sqcq_kick_wakeup);
+       vq->kick_wqh = NULL;
+
+       if (vq->kick) {
+               __poll_t mask;
+
+               init_poll_funcptr(&vq->kick_pt, vhost_sqcq_kick_ptable_func);
+               mask = vfs_poll(vq->kick, &vq->kick_pt);
+               /* eventfd counter is never consumed; verify stale EPOLLIN
+                * against sq->idx before acting; real kicks wake via
+                * the registered wait queue.
+                */
+               if (mask & EPOLLIN) {
+                       __virtio64 sq_val;
+
+                       if (vq->sq && !get_user(sq_val, &vq->sq->idx)) {
+                               smp_rmb();
+                               if ((__force u16)sq_val != vq->last_avail_idx)
+                                       vhost_sqcq_kick_wakeup(
+                                               &vq->kick_wait, 0, 0,
+                                               poll_to_key(mask));
+                       }
+               }
+       }
+}
+
+static void vhost_sqcq_kick_unregister(struct vhost_virtqueue *vq)
+{
+       if (vq->kick_wqh) {
+               remove_wait_queue(vq->kick_wqh, &vq->kick_wait);
+               vq->kick_wqh = NULL;
+       }
+       WRITE_ONCE(vq->kick_received, false);
+}
+
+static int vhost_sqcq_poll_thread(void *data)
+{
+       struct vhost_virtqueue *vq = data;
+       struct vhost_dev *dev = vq->dev;
+       u16 new_sq_idx;
+       __virtio64 sq_idx_val;
+       __u8 sq_need_wakeup_val = 0;
+       ktime_t ts2;
+       ktime_t last_io_ts = 0;
+       ktime_t stats_window = ktime_get();
+       u64 stats_cq = 0;
+       u64 stats_lat = 0;
+       u64 stats_inflight_sum = 0;
+       u64 stats_inflight_n = 0;
+       u64 stats_multi_cnt = 0;
+       u64 stats_sleep_cnt = 0;
+       u64 stats_wake_kick = 0;
+       u64 stats_wake_complete = 0;
+       u64 stats_wake_timeout = 0;
+       u64 ema_ns_val;
+       u64 spin_ns_val;
+       unsigned long _timeout_jiffies;
+       u64 _timeout_us;
+
+       kthread_use_mm(dev->mm);
+       complete(&vq->poll_started);
+
+       vq->stat_completions = 0;
+       vq->lat_scsi_ns = 0;
+       vq->ema_latency_ns = 0;
+       vq->ema_interval_ns = 0;
+       ts2 = ktime_get();
+
+       while (!READ_ONCE(vq->poll_stop)) {
+
+               stats_inflight_sum += (u16)(vq->last_avail_idx - 
vq->last_used_idx);
+               stats_inflight_n++;
+               stats_multi_cnt += ((atomic_read(&sqcq_active_io_vqs) > 1) ? 1 
: 0);
+
+               // 10's output windows.
+               if (ktime_to_ns(ktime_sub(ktime_get(), stats_window)) >=
+                           (s64)10 * NSEC_PER_SEC) {
+                       if (stats_cq > 0) {
+                               int vi;
+
+                               for (vi = 0; vi < dev->nvqs; vi++)
+                                       if (dev->vqs[vi] == vq)
+                                               break;
+                               pr_info("vhost-sqcq: vq[%d] cq=%llu avg=%lluns 
ema_lat=%lluns interval=%lluns avg_inflight=%llu multi=%llu%% sleep=%llu 
wk_kick=%llu%% wk_cmp=%llu%% wk_to=%llu%%\n",
+                                      vi, stats_cq,
+                                      div64_u64(stats_lat, stats_cq),
+                                      vq->ema_latency_ns,
+                                      vq->ema_interval_ns,
+                                      stats_inflight_n > 0 ?
+                                      div64_u64(stats_inflight_sum, 
stats_inflight_n) : 0,
+                                      div64_u64(stats_multi_cnt*100, 
stats_inflight_n),
+                                      stats_sleep_cnt,
+                                      stats_sleep_cnt > 0 ?
+                                      div64_u64(stats_wake_kick*100, 
stats_sleep_cnt) : 0,
+                                      stats_sleep_cnt > 0 ?
+                                      div64_u64(stats_wake_complete*100, 
stats_sleep_cnt) : 0,
+                                      stats_sleep_cnt > 0 ?
+                                      div64_u64(stats_wake_timeout*100, 
stats_sleep_cnt) : 0);
+                       }
+                       stats_window = ktime_get();
+                       stats_cq = 0;
+                       stats_lat = 0;
+                       stats_inflight_sum = 0;
+                       stats_inflight_n = 0;
+                       stats_multi_cnt = 0;
+                       stats_sleep_cnt = 0;
+                       stats_wake_kick = 0;
+                       stats_wake_complete = 0;
+                       stats_wake_timeout = 0;
+               }
+
+               /* Backstop cleanup: drop sq_was_active and reset EMAs once a VQ
+                * has been idle (no submission) for a fixed interval.
+                */
+               if (vq->sq_was_active && last_io_ts > 0 &&
+                   ktime_to_ns(ktime_sub(ktime_get(), last_io_ts)) >
+                           (s64)SQCQ_STALE_TIMEOUT_NS) {
+                       vq->sq_was_active = false;
+                       atomic_dec(&sqcq_active_io_vqs);
+                       if (vq->poll_cpu >= 0)
+                               cpumask_clear_cpu(vq->poll_cpu, 
&sqcq_poll_active_cpu_mask);
+                       WRITE_ONCE(vq->ema_interval_ns, 0);
+                       WRITE_ONCE(vq->ema_latency_ns, 0);
+               }
+
+               /* complete*/
+               if (atomic_read(&vq->completion_pending) && vq->poll_complete) {
+                       smp_rmb();
+                       atomic_set(&vq->completion_pending, 0);
+                       vq->poll_complete(vq);
+                       if (vq->stat_completions > 0) {
+                               u64 avg = div64_u64(vq->lat_scsi_ns,
+                                                   vq->stat_completions);
+
+                               if (vq->ema_latency_ns == 0)
+                                       vq->ema_latency_ns = avg;
+                               else
+                                       vq->ema_latency_ns =
+                                               vq->ema_latency_ns -
+                                               vq->ema_latency_ns / 8 +
+                                               avg / 8;
+                               stats_cq += vq->stat_completions;
+                               stats_lat += vq->lat_scsi_ns;
+                               vq->lat_scsi_ns = 0;
+                               vq->stat_completions = 0;
+                       }
+               }
+
+               if (get_user(sq_idx_val, &vq->sq->idx)) {
+                       int fault_retries = 3;
+
+                       while (fault_retries-- > 0) {
+                               if (!get_user(sq_idx_val, &vq->sq->idx))
+                                       break;
+                               schedule_timeout_idle(1);
+                       }
+                       if (fault_retries < 0)
+                               schedule_timeout_idle(HZ / 100);
+                       continue;
+               }
+               /* Acquire: see all prior Guest writes (descriptors). */
+               smp_rmb();
+               new_sq_idx = (__force u16)sq_idx_val;
+
+               if (new_sq_idx != vq->last_avail_idx) {
+                       vq->handle_kick(&vq->poll.work);
+                       ts2 = ktime_get();
+                       if (last_io_ts > 0) {
+                               u64 _interval = ktime_to_ns(ktime_sub(ts2, 
last_io_ts));
+                               if (vq->ema_interval_ns == 0)
+                                       vq->ema_interval_ns = _interval;
+                               else
+                                       vq->ema_interval_ns =
+                                               vq->ema_interval_ns -
+                                               vq->ema_interval_ns / 8 +
+                                               _interval / 8;
+                       }
+                       last_io_ts = ts2;
+                       if (!vq->sq_was_active) {
+                               vq->sq_was_active = true;
+                               atomic_inc(&sqcq_active_io_vqs);
+                               if (vq->poll_cpu >= 0)
+                                       cpumask_set_cpu(vq->poll_cpu, 
&sqcq_poll_active_cpu_mask);
+                       }
+                       sq_need_wakeup_val = 0;
+                       smp_mb();
+                       put_user(sq_need_wakeup_val, &vq->sq->sq_need_wakeup);
+               }
+
+
+               if (vq->last_avail_idx == vq->last_used_idx) {
+                       /* Spin for a fixed window to catch next submission via
+                        * sq->idx polling; need_resched+cond_resched prevents
+                        * starving other tasks on this CPU.
+                        */
+                       spin_ns_val = SQCQ_ACTIVE_TIMEOUT_NS;
+                       if (last_io_ts > 0 && ktime_to_ns(ktime_sub(
+                           ktime_get(), last_io_ts)) < spin_ns_val) {
+                               if (need_resched())
+                                       cond_resched();
+                               cpu_relax();
+                               continue;
+                       }
+               /* Clear sq_was_active to keep active_io_vqs and
+                * cpu mask fresh. Has-inflight path keeps the
+                * flag for TCM completion steering.
+                */
+                       if (vq->sq_was_active) {
+                               vq->sq_was_active = false;
+                               atomic_dec(&sqcq_active_io_vqs);
+                               if (vq->poll_cpu >= 0)
+                                       cpumask_clear_cpu(vq->poll_cpu, 
&sqcq_poll_active_cpu_mask);
+                       }
+               } else {
+               /* Spin within SQCQ_SPIN_EMA_MULT * ema_latency (with
+                * cooperative yield), then sleep.  cond_resched handles
+                * multi-VQ fairness without a separate yield heuristic.
+                */
+                       ema_ns_val = READ_ONCE(vq->ema_latency_ns);
+                       spin_ns_val = ema_ns_val > 0 ?
+                               (u64)SQCQ_SPIN_EMA_MULT * ema_ns_val :
+                               2 * NSEC_PER_USEC;  /* fallback when EMA 
unknown */
+
+                       if (ktime_to_ns(ktime_sub(
+                           ktime_get(), last_io_ts)) < spin_ns_val) {
+                               if (need_resched())
+                                       cond_resched();
+                               cpu_relax();
+                               continue;
+                       }
+               }
+
+               ema_ns_val = READ_ONCE(vq->ema_latency_ns);
+
+               if (ema_ns_val == 0) {
+                       _timeout_jiffies = usecs_to_jiffies(
+                               SQCQ_IDLE_TIMEOUT_US_MAX);
+               } else {
+                       _timeout_us = div64_u64(
+                               ema_ns_val,
+                               (u64)NSEC_PER_USEC / SQCQ_SLEEP_EMA_MULT);
+
+                       _timeout_us = clamp(_timeout_us,
+                               10ULL,
+                               (u64)SQCQ_IDLE_TIMEOUT_US_MAX);
+                       _timeout_jiffies = usecs_to_jiffies(
+                               _timeout_us);
+                       if (_timeout_jiffies == 0)
+                               _timeout_jiffies = 1;
+               }
+
+               /* Set NEED_WAKEUP, then double-check sq->idx before sleeping. 
*/
+               smp_wmb();
+               sq_need_wakeup_val = 1;
+               put_user(sq_need_wakeup_val,
+                       &vq->sq->sq_need_wakeup);
+               smp_mb();
+
+               if (get_user(sq_idx_val, &vq->sq->idx)) {
+                       int fault_retries = 3;
+
+                       while (fault_retries-- > 0) {
+                               if (!get_user(sq_idx_val, &vq->sq->idx))
+                                       break;
+                               schedule_timeout_idle(1);
+                       }
+                       if (fault_retries < 0)
+                               schedule_timeout_idle(HZ / 100);
+                       continue;
+               }
+               smp_rmb();
+               new_sq_idx = (__force u16)sq_idx_val;
+
+               if (new_sq_idx != vq->last_avail_idx) {
+                       sq_need_wakeup_val = 0;
+                       smp_mb();
+                       put_user(sq_need_wakeup_val,
+                               &vq->sq->sq_need_wakeup);
+                       continue;
+               }
+
+               ++stats_sleep_cnt;
+               vhost_sqcq_kick_register(vq);
+               wait_event_interruptible_timeout(
+                       vq->poll_wait,
+                       READ_ONCE(vq->poll_stop) ||
+                       READ_ONCE(vq->kick_received) ||
+                       atomic_read(&vq->completion_pending),
+                       _timeout_jiffies);
+               /* Wake reason stats: snapshot before unregister clears 
kick_received */
+               {
+                       bool _kicked = READ_ONCE(vq->kick_received);
+                       bool _completed = atomic_read(&vq->completion_pending);
+
+                       if (_kicked)
+                               stats_wake_kick++;
+                       if (_completed)
+                               stats_wake_complete++;
+                       if (!_kicked && !_completed)
+                               stats_wake_timeout++;
+               }
+               vhost_sqcq_kick_unregister(vq);
+
+               sq_need_wakeup_val = 0;
+               put_user(sq_need_wakeup_val,
+                       &vq->sq->sq_need_wakeup);
+       }
+
+       complete(&vq->poll_stopped);
+       kthread_unuse_mm(dev->mm);
+       return 0;
+}
+
+int vhost_sqcq_poll_start(struct vhost_virtqueue *vq)
+{
+       if (!vq->sq || !vq->cq || !vq->handle_kick)
+               return -EINVAL;
+
+       vhost_poll_stop(&vq->poll);
+
+       WRITE_ONCE(vq->poll_stop, false);
+       reinit_completion(&vq->poll_started);
+       reinit_completion(&vq->poll_stopped);
+
+       int vi;
+
+       for (vi = 0; vi < vq->dev->nvqs; vi++)
+               if (vq->dev->vqs[vi] == vq)
+                       break;
+       vq->poll_task = kthread_run(vhost_sqcq_poll_thread, vq,
+                                   "vhost-sqcq-%d-%d",
+                                   task_pid_nr(current),
+                                   vi);
+       if (IS_ERR(vq->poll_task)) {
+               int ret = PTR_ERR(vq->poll_task);
+
+               vq->poll_task = NULL;
+               if (vq->kick)
+                       vhost_poll_start(&vq->poll, vq->kick);
+               return ret;
+       }
+
+       wait_for_completion(&vq->poll_started);
+
+       /* Enable sqcq mode only after poll_task is valid and the
+        * thread has signaled readiness.  Before this point,
+        * vhost_signal() routes completions via eventfd (old path).
+        */
+       smp_mb();
+       WRITE_ONCE(vq->sqcq_poll, true);
+
+       if (vq->poll_task) {
+               int cpu;
+               int start = atomic_inc_return(&sqcq_poll_cpu_rr);
+               int target_cpu = -1;
+               int online = num_online_cpus();
+
+               if (online > 0)
+                       start = start % online;
+               else
+                       start = 0;
+
+               for_each_online_cpu(cpu) {
+                       if (start-- <= 0) {
+                               target_cpu = cpu;
+                               break;
+                       }
+               }
+               if (target_cpu < 0) {
+                       for_each_online_cpu(cpu) {
+                               target_cpu = cpu;
+                               break;
+                       }
+               }
+
+               if (target_cpu >= 0 && target_cpu < nr_cpu_ids &&
+                   cpu_online(target_cpu)) {
+                       int vi;
+
+                       set_cpus_allowed_ptr(vq->poll_task, 
cpumask_of(target_cpu));
+                       for (vi = 0; vi < vq->dev->nvqs; vi++)
+                               if (vq->dev->vqs[vi] == vq)
+                                       break;
+                       pr_info("vhost-sqcq: vq[%d] poll thread bound to 
cpu%d\n",
+                               vi, target_cpu);
+                       vq->poll_cpu = target_cpu;
+               }
+       }
+
+       return 0;
+}
+EXPORT_SYMBOL_GPL(vhost_sqcq_poll_start);
+
+void vhost_sqcq_poll_stop(struct vhost_virtqueue *vq)
+{
+       if (!vq->poll_task)
+               return;
+
+       WRITE_ONCE(vq->poll_stop, true);
+       smp_mb();
+       wake_up_process(vq->poll_task);
+       wake_up(&vq->poll_wait);
+       wait_for_completion(&vq->poll_stopped);
+       vq->poll_task = NULL;
+       WRITE_ONCE(vq->sqcq_poll, false);
+       if (vq->poll_cpu >= 0) {
+               cpumask_clear_cpu(vq->poll_cpu, &sqcq_poll_active_cpu_mask);
+               vq->poll_cpu = -1;
+       }
+       smp_mb();
+
+       if (vq->kick)
+               vhost_poll_start(&vq->poll, vq->kick);
+       vq->sq = NULL;
+       vq->cq = NULL;
+}
+EXPORT_SYMBOL_GPL(vhost_sqcq_poll_stop);
+
+/* Flush residual completions after stop.  Must be called outside vq->mutex. */
+void vhost_sqcq_poll_flush(struct vhost_virtqueue *vq)
+{
+       if (WARN_ON_ONCE(!vq->poll_complete))
+               return;
+
+       atomic_set(&vq->completion_pending, 0);
+       vq->poll_complete(vq);
+}
+EXPORT_SYMBOL_GPL(vhost_sqcq_poll_flush);
+
+/* Pick a CPU for TCM completion: prefer a free CPU (no poll thread),
+ * searching from hint_cpu+1 with wrap. Fallback to next-online
+ * if every CPU has a poll thread.
+ */
+int vhost_sqcq_pick_completion_cpu(int hint_cpu)
+{
+       int first, cpu, target = -1;
+
+       first = cpumask_next(hint_cpu, cpu_online_mask);
+       if (first >= nr_cpu_ids)
+               first = cpumask_first(cpu_online_mask);
+
+       cpu = first;
+       do {
+               if (!cpumask_test_cpu(cpu, &sqcq_poll_active_cpu_mask)) {
+                       target = cpu;
+                       break;
+               }
+               cpu = cpumask_next(cpu, cpu_online_mask);
+               if (cpu >= nr_cpu_ids)
+                       cpu = cpumask_first(cpu_online_mask);
+       } while (cpu != first);
+
+       if (target < 0)
+               target = first;
+
+       return target;
+}
+EXPORT_SYMBOL_GPL(vhost_sqcq_pick_completion_cpu);
+
+static long vhost_vring_set_sqcq_addr(struct vhost_dev *d,
+                                      struct vhost_virtqueue *vq,
+                                      void __user *argp)
+{
+       struct vhost_vring_sqcq_addr a;
+
+       if (copy_from_user(&a, argp, sizeof(a)))
+               return -EFAULT;
+
+       if ((u64)(unsigned long)a.sq_user_addr != a.sq_user_addr ||
+           (u64)(unsigned long)a.cq_user_addr != a.cq_user_addr)
+               return -EFAULT;
+
+       if (a.sq_user_addr & 7 || a.cq_user_addr & 7)
+               return -EINVAL;
+
+       if (vq->private_data) {
+               if (!access_ok((void __user *)(unsigned long)a.sq_user_addr,
+                              sizeof(struct vring_sq)) ||
+                   !access_ok((void __user *)(unsigned long)a.cq_user_addr,
+                              sizeof(struct vring_cq)))
+                       return -EINVAL;
+       }
+
+       vq->sq = (struct vring_sq __user *)(unsigned long)a.sq_user_addr;
+       vq->cq = (struct vring_cq __user *)(unsigned long)a.cq_user_addr;
+
+       /* SET_ENDPOINT may run before SET_FEATURES/SQCQ_ADDR, so poll-start
+        * can fail there; this is the canonical trigger. Skip VQs without
+        * poll_complete (ctrl/event VQs) — they stay interrupt-driven.
+        */
+       if (vq->private_data && vq->sq && vq->cq &&
+           !READ_ONCE(vq->sqcq_poll) &&
+           vq->poll_complete &&
+           vhost_has_feature(vq, VIRTIO_F_SQCQ_POLL)) {
+               long ret = vhost_sqcq_poll_start(vq);
+               if (ret)
+                       return ret;
+       }
+
+       return 0;
+}
+
 static long vhost_vring_set_addr(struct vhost_dev *d,
                                 struct vhost_virtqueue *vq,
                                 void __user *argp)
@@ -2244,6 +2810,9 @@ long vhost_vring_ioctl(struct vhost_dev *d, unsigned int 
ioctl, void __user *arg
                if (copy_to_user(argp, &s, sizeof(s)))
                        r = -EFAULT;
                break;
+       case VHOST_SET_VRING_SQCQ_ADDR:
+               r = vhost_vring_set_sqcq_addr(d, vq, argp);
+               break;
        default:
                r = -ENOIOCTLCMD;
        }
@@ -2256,7 +2825,7 @@ long vhost_vring_ioctl(struct vhost_dev *d, unsigned int 
ioctl, void __user *arg
        if (filep)
                fput(filep);
 
-       if (pollstart && vq->handle_kick)
+       if (pollstart && vq->handle_kick && !READ_ONCE(vq->sqcq_poll))
                r = vhost_poll_start(&vq->poll, vq->kick);
 
        mutex_unlock(&vq->mutex);
@@ -2938,8 +3507,12 @@ int vhost_get_vq_desc_n(struct vhost_virtqueue *vq,
                *ndesc = c;
 
        /* Assume notifications from guest are disabled at this point,
-        * if they aren't we would need to update avail_event index. */
-       BUG_ON(!(vq->used_flags & VRING_USED_F_NO_NOTIFY));
+        * if they aren't we would need to update avail_event index.
+        * In SQCQ poll mode, vhost_disable_notify is a no-op so
+        * VRING_USED_F_NO_NOTIFY is never set; skip the check.
+        */
+       WARN_ON_ONCE(!vq->sqcq_poll &&
+                    !(vq->used_flags & VRING_USED_F_NO_NOTIFY));
        return head;
 }
 EXPORT_SYMBOL_GPL(vhost_get_vq_desc_n);
@@ -3167,6 +3740,16 @@ static bool vhost_notify(struct vhost_dev *dev, struct 
vhost_virtqueue *vq)
 /* This actually signals the guest, using eventfd. */
 void vhost_signal(struct vhost_dev *dev, struct vhost_virtqueue *vq)
 {
+       if (READ_ONCE(vq->sqcq_poll) && vq->cq) {
+               atomic_set(&vq->completion_pending, 1);
+               if (!READ_ONCE(vq->poll_stop)) {
+                       smp_mb();
+                       if (!task_is_running(vq->poll_task))
+                               wake_up(&vq->poll_wait);
+               }
+               return;
+       }
+
        /* Signal the Guest tell them we used something up. */
        if (vq->call_ctx.ctx && vhost_notify(dev, vq))
                eventfd_signal(vq->call_ctx.ctx);
@@ -3213,6 +3796,9 @@ EXPORT_SYMBOL_GPL(vhost_vq_avail_empty);
 /* OK, now we need to know about added descriptors. */
 bool vhost_enable_notify(struct vhost_dev *dev, struct vhost_virtqueue *vq)
 {
+       if (READ_ONCE(vq->sqcq_poll))
+               return false;
+
        int r;
 
        if (!(vq->used_flags & VRING_USED_F_NO_NOTIFY))
@@ -3249,6 +3835,9 @@ EXPORT_SYMBOL_GPL(vhost_enable_notify);
 /* We don't need to be notified again. */
 void vhost_disable_notify(struct vhost_dev *dev, struct vhost_virtqueue *vq)
 {
+       if (READ_ONCE(vq->sqcq_poll))
+               return;
+
        int r;
 
        if (vq->used_flags & VRING_USED_F_NO_NOTIFY)
diff --git a/drivers/vhost/vhost.h b/drivers/vhost/vhost.h
index 4fe99765c5c73..b9c15961f5a18 100644
--- a/drivers/vhost/vhost.h
+++ b/drivers/vhost/vhost.h
@@ -111,6 +111,7 @@ struct vhost_virtqueue {
 
        /* The routine to call when the Guest pings us, or timeout. */
        vhost_work_fn_t handle_kick;
+       void (*poll_complete)(struct vhost_virtqueue *vq);
 
        /* Last available index we saw.
         * Values are limited to 0x7fff, and the high bit is used as
@@ -164,6 +165,27 @@ struct vhost_virtqueue {
        bool user_be;
 #endif
        u32 busyloop_timeout;
+
+       /* SQ/CQ polling mode */
+       bool sqcq_poll;
+       struct vring_sq __user *sq;
+       struct vring_cq __user *cq;
+       struct task_struct *poll_task;
+       bool poll_stop;
+       wait_queue_head_t poll_wait;
+       struct completion poll_started;
+       struct completion poll_stopped;
+       bool kick_received;             /* Set when kick eventfd wakes us */
+       atomic_t completion_pending;    /* Set by vhost_signal, poll thread 
atomically clears */
+       wait_queue_head_t *kick_wqh;    /* kick eventfd wait queue head */
+       wait_queue_entry_t kick_wait;   /* Wait entry on kick eventfd wq */
+       poll_table kick_pt;             /* poll_table for kick eventfd */
+       u64             stat_completions;
+       u64             lat_scsi_ns;
+       u64             ema_interval_ns;
+       u64             ema_latency_ns;
+       bool            sq_was_active;
+       int             poll_cpu;       /* CPU this VQ's poll thread is pinned 
to, -1 = unpinned */
 };
 
 struct vhost_msg_node {
@@ -256,6 +278,10 @@ void vhost_add_used_and_signal_n(struct vhost_dev *, 
struct vhost_virtqueue *,
 void vhost_signal(struct vhost_dev *, struct vhost_virtqueue *);
 void vhost_disable_notify(struct vhost_dev *, struct vhost_virtqueue *);
 bool vhost_vq_avail_empty(struct vhost_dev *, struct vhost_virtqueue *);
+int vhost_sqcq_poll_start(struct vhost_virtqueue *vq);
+void vhost_sqcq_poll_stop(struct vhost_virtqueue *vq);
+void vhost_sqcq_poll_flush(struct vhost_virtqueue *vq);
+int vhost_sqcq_pick_completion_cpu(int hint_cpu);
 bool vhost_enable_notify(struct vhost_dev *, struct vhost_virtqueue *);
 
 int vhost_log_write(struct vhost_virtqueue *vq, struct vhost_log *log,
@@ -294,7 +320,8 @@ void vhost_iotlb_map_free(struct vhost_iotlb *iotlb,
        VIRTIO_RING_F_EVENT_IDX, \
        VHOST_F_LOG_ALL, \
        VIRTIO_F_ANY_LAYOUT, \
-       VIRTIO_F_VERSION_1
+       VIRTIO_F_VERSION_1, \
+       VIRTIO_F_SQCQ_POLL
 
 static inline u64 vhost_features_u64(const int *features, int size, int idx)
 {
diff --git a/include/uapi/linux/vhost.h b/include/uapi/linux/vhost.h
index c57674a6aa0db..dacb0e1fdc4bc 100644
--- a/include/uapi/linux/vhost.h
+++ b/include/uapi/linux/vhost.h
@@ -123,6 +123,10 @@
 #define VHOST_SET_BACKEND_FEATURES _IOW(VHOST_VIRTIO, 0x25, __u64)
 #define VHOST_GET_BACKEND_FEATURES _IOR(VHOST_VIRTIO, 0x26, __u64)
 
+/* Set SQ/CQ doorbell addresses for polling mode */
+#define VHOST_SET_VRING_SQCQ_ADDR _IOW(VHOST_VIRTIO, 0x27, \
+                                       struct vhost_vring_sqcq_addr)
+
 /* VHOST_NET specific defines */
 
 /* Attach virtio net ring to a raw socket, or tap device.
diff --git a/include/uapi/linux/vhost_types.h b/include/uapi/linux/vhost_types.h
index 1c39cc5f5a31b..6514c1b5be20b 100644
--- a/include/uapi/linux/vhost_types.h
+++ b/include/uapi/linux/vhost_types.h
@@ -47,6 +47,12 @@ struct vhost_vring_addr {
        __u64 log_guest_addr;
 };
 
+struct vhost_vring_sqcq_addr {
+       unsigned int index;
+       __u64 sq_user_addr;     /* SQ doorbell HVA */
+       __u64 cq_user_addr;     /* CQ doorbell HVA */
+};
+
 struct vhost_worker_state {
        /*
         * For VHOST_NEW_WORKER the kernel will return the new vhost_worker id.
-- 
2.34.1


Reply via email to