מערכת וירטואלית והתקן Virtio¶
מי מריץ ומי מספק את העולם¶
במכונה וירטואלית המעבד האורח יכול לבצע קוד בעזרת תמיכת המעבד והמנגנון שבקרנל, אך עדיין צריך מישהו שיבנה עבורו זיכרון, התקנים ותהליך אתחול. מנהל המכונה במרחב המשתמש מגדיר את העולם הזה ומטפל באירועים שדורשים את התערבותו. לכן הפעלת מעבד וירטואלי אינה שקולה להפעלת מערכת מלאה.
הממשק של KVM מבוסס על מתארים. מתאר המערכת מאפשר שאילתות ויצירת מכונה, מתאר המכונה מאפשר הגדרת זיכרון ויצירת מעבדים, ומתאר המעבד מאפשר הגדרת מצב והרצה. היציאה מהרצת האורח יכולה לדווח על קלט ופלט, גישה להתקן ממופה, עצירה או שגיאה. לא כל קריאת מערכת של תהליך בתוך האורח יוצאת בהכרח למנהל במרחב המשתמש של המארח.
אורח בן כמה הוראות¶
נבנה תוכנית סי קטנה על מארח לינוקס בארכיטקטורת המעבדה עם גישה מותרת ל־/dev/kvm. היא אינה טוענת קרנל ואינה מדמה מחשב כללי. האורח כותב בית אחד לפתח קלט ופלט ואז עוצר. כך נוכל לראות את הגבול בין ביצוע במעבד הווירטואלי לבין טיפול בהתקן במארח.
#include <linux/kvm.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void)
{
int system = -1, vm = -1, cpu = -1, status = 1;
int run_size = 0;
void *memory = MAP_FAILED;
struct kvm_run *run = MAP_FAILED;
struct kvm_sregs special;
struct kvm_regs registers = { .rip = 0x1000, .rflags = 2 };
unsigned char code[] = { 0xba, 0xf8, 0x03, 0xb0, 0x41, 0xee, 0xf4 };
struct kvm_userspace_memory_region region = {
.slot = 0, .guest_phys_addr = 0x1000, .memory_size = 4096,
};
system = open("/dev/kvm", O_RDWR | O_CLOEXEC);
if (system < 0) goto out;
if (ioctl(system, KVM_GET_API_VERSION, 0) != 12) goto out;
if (ioctl(system, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY) <= 0) goto out;
vm = ioctl(system, KVM_CREATE_VM, 0);
if (vm < 0) goto out;
memory = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (memory == MAP_FAILED) goto out;
memcpy(memory, code, sizeof(code));
region.userspace_addr = (uintptr_t)memory;
if (ioctl(vm, KVM_SET_USER_MEMORY_REGION, ®ion) < 0) goto out;
cpu = ioctl(vm, KVM_CREATE_VCPU, 0);
if (cpu < 0) goto out;
run_size = ioctl(system, KVM_GET_VCPU_MMAP_SIZE, 0);
if (run_size < (int)sizeof(*run)) goto out;
run = mmap(NULL, run_size, PROT_READ | PROT_WRITE, MAP_SHARED, cpu, 0);
if (run == MAP_FAILED) goto out;
if (ioctl(cpu, KVM_GET_SREGS, &special) < 0) goto out;
special.cs.base = 0;
special.cs.selector = 0;
if (ioctl(cpu, KVM_SET_SREGS, &special) < 0) goto out;
if (ioctl(cpu, KVM_SET_REGS, ®isters) < 0) goto out;
for (;;) {
if (ioctl(cpu, KVM_RUN, 0) < 0) {
if (errno == EINTR) continue;
goto out;
}
if (run->exit_reason == KVM_EXIT_HLT) { status = 0; break; }
if (run->exit_reason == KVM_EXIT_IO &&
run->io.direction == KVM_EXIT_IO_OUT && run->io.port == 0x3f8 &&
run->io.size == 1 && run->io.count == 1 &&
run->io.data_offset < (uint64_t)run_size) {
unsigned char *byte = (unsigned char *)run + run->io.data_offset;
printf("guest output: %c\n", *byte);
} else {
fprintf(stderr, "unsupported exit=%u\n", run->exit_reason);
goto out;
}
}
out:
if (status) fprintf(stderr, "VM setup or execution failed; errno=%d\n", errno);
if (run != MAP_FAILED) munmap(run, run_size);
if (cpu >= 0) close(cpu);
if (vm >= 0) close(vm);
if (memory != MAP_FAILED) munmap(memory, 4096);
if (system >= 0) close(system);
return status;
}
נבנה עם cc -O2 -Wall tiny_vm.c -o tiny_vm ונריץ בסביבה שתומכת בכך. התוצאה הצפויה היא בית התצוגה שהקוד האורח כתב וסיום תקין. אם המערכת עצמה נמצאת באורח ללא וירטואליזציה מקוננת, ייתכן שהממשק אינו זמין. זו מגבלת סביבת הניסוי ולא ראיה שהקוד האורח שגוי.
אזור הזיכרון של המארח מחובר לכתובת פיזית אורחת. הוא נשאר חי עד סיום השימוש במכונה. כתובת פיזית אורחת אינה מצביע שמותר להעביר ישירות לפונקציה של המארח. במנהל מלא נדרשת טבלת מיפוי, בדיקת טווחים וטיפול בחורים בין אזורי זיכרון.
הטיפול בכתיבת פתח הקלט והפלט הוא דגם מינימלי של התקן. החזרה להרצה לאחר הטיפול משלימה את ההתקדמות לפי חוזה הממשק. בקוד אמיתי נטפל במפורש בכל סיבת יציאה נתמכת ולא נניח שכל יציאה היא בקשה מהתקן שלנו. ערך השגיאה המודפס בדוגמה הוא מידע נוסף בלבד במקרים של דחיית יכולת, שבהם לא בהכרח נקבעה שגיאת מערכת חדשה.
התקן וירטואלי אינו חייב לחקות חומרה ישנה¶
מפרט Virtio מגדיר התקנים ותורים שנועדו לעבודה בין אורח למימוש התקן. לפני התחלת עבודה הצדדים מסכימים על יכולות. הדרייבר קורא יכולות, בוחר תת קבוצה נתמכת, מאשר שההתקן קיבל אותה ורק אז מתקדם למצב שבו אפשר להגיש עבודה. שימוש בתכונה שלא סוכמה שובר את החוזה גם אם שני הצדדים יודעים עליה באופן כללי.
בטבעת מפוצלת קיימת טבלת תיאורים, טבעת פריטים זמינים וטבעת פריטים שהושלמו. תיאור מכיל כתובת בתחום האורח, אורך, דגלים ולעיתים קישור לתיאור הבא. דגל כתיבה מציין מי רשאי לכתוב לבאפר לפי המפרט. פרסום אינדקס לפני השלמת כתיבת התיאורים עלול לחשוף למקבל מבנה חלקי, ולכן סדר הזיכרון הוא חלק מהפרוטוקול.
טבעת ארוזה משתמשת בארגון אחר ובסיביות מחזור. אין להעתיק אליה חישוב אינדקסים של טבעת מפוצלת בלי לקרוא את החוזה. בשני המקרים מספרי האינדקסים יכולים להקיף את התחום, וההשוואה חייבת להתאים לרוחב ולכללי המפרט.
קלט אורח הוא קלט שצריך לבדוק¶
מנהל ההתקן במארח צריך לבדוק שהשרשרת אינה מחזורית או ארוכה מדי, שהאינדקסים בתחום ושהכתובות מתורגמות לאזורי זיכרון תקפים. חיבור כתובת ואורך חייב להשתמש בחשבון בדוק כדי למנוע גלישה. כיוון גישה ואורך תשובה נבדקים לפני שימוש, גם אם הדרייבר הרגיל באורח תמיד שולח נתונים תקינים.
דוגמת הראסט הבאה בודקת טווח בתוך אזור אורח יחיד. היא אינה מממשת תור וירטואלי מלא, אך מדגימה את ההפרדה בין כתובת אורח לאינדקס בטוח במארח.
fn guest_range(memory: &[u8], base: u64, address: u64, length: u32)
-> Result<&[u8], &'static str>
{
let offset = address.checked_sub(base).ok_or("below region")?;
let end = offset.checked_add(u64::from(length)).ok_or("overflow")?;
let start = usize::try_from(offset).map_err(|_| "host width")?;
let end = usize::try_from(end).map_err(|_| "host width")?;
memory.get(start..end).ok_or("outside region")
}
fn main() {
let memory = vec![0u8; 4096];
assert_eq!(guest_range(&memory, 0x1000, 0x1010, 16).unwrap().len(), 16);
assert!(guest_range(&memory, 0x1000, 0xfff, 1).is_err());
assert!(guest_range(&memory, 0x1000, u64::MAX, 32).is_err());
}
לאחר פרסום השלמה הדרייבר יכול למחזר באפר בהתאם לכללי הפרוטוקול. הודעת פסיקה היא מנגנון התראה ואינה הבעלות עצמה. אם הודעה אוחדה או דוכאה, הצד השני עדיין חייב להיות מסוגל לגלות עבודה דרך מצב הטבעת. כך חוזרים אותם עקרונות של פרסום, מוכנות ובעלות גם בתוך מכונה וירטואלית.
להרחבה ניעזר בממשק הווירטואליזציה של הקרנל ובמפרט ההתקנים הווירטואליים.