1. 程式人生 > 其它 >virtio分析(三) —— virtio-balloon guest側驅動

virtio分析(三) —— virtio-balloon guest側驅動

一: 概要

在後端模擬出balloon裝置後,gustos在啟動時會掃描到此裝置,遵循linux裝置模型呼叫裝置的初始化工作。Virtio-balloon屬於 virtio體系,很多工作的細節需要再分析virtio的工作流程,本章暫且只分析balloon的行為,涉及virtio的部分插樁分析向後再補充分析。 balloon執行流程如下:

二:驅動建立

2.1 驅動註冊

Linux裝置驅動模型中,各驅動可以按匯流排類別進行劃分,且每個匯流排類別下可以掛載“驅動”和“裝置”兩類物件。核心就維護了這樣一張“匯流排”到“驅動和裝置”的總表,每當一個新驅動加進核心時,核心會掃描該驅動所掛載總線上的所有裝置,並通過比對驅動中的id_table欄位和裝置配置空間中的Device ID,如果相同則代表該驅動可以為該裝置服務,那核心就會針對該裝置呼叫匯流排的probe函式(如果匯流排沒有probe函式,再呼叫驅動的probe函式)。
另外一種情況是往總線上插入一個新裝置,核心同樣會掃描總線上的所有驅動,看哪個驅動匹配該裝置,如果匹配也對該裝置呼叫匯流排的probe函式(如果匯流排沒有probe函式,再呼叫驅動的probe函式)。

  Linux核心中前端程式碼主要包括driver/virtio目錄下相關檔案及driver/virtio_balloon.c,最終生成的核心模組有virtio.ko,virtio_ring.ko,virtio_pci.ko和virtio_balloon.ko。
  由於virtio-balloon-pci裝置是virtio-pci裝置,而virtio-pci裝置又是pci裝置,所以virtio-pci裝置的驅動會註冊到pci總線上面,因此,整個初始化過程如下:
  (1)核心會首先找到virito-pci.ko這個驅動模組,並依次載入virtio.ko,virtio-ring.ko和virtio_pci.ko (virtio_pci.ko依賴前兩個模組)執行其模組初始化函式,其中,virtio.ko模組會在系統中註冊一種新的匯流排型別virtio匯流排,virtio_pci的初始化函式會呼叫其註冊的virtio_pci_probe函式;
  (2)virtio_pci_probe註冊一個virtio裝置(register_virtio_device);
  (3)核心再次為這個virtio裝置搜尋驅動模組,最終找到virtio_balloon.ko並載入呼叫其模組初始化函式;
  (4)virtio_balloon初始化函式在virtio總線上添加了virtio_balloon驅動並呼叫了匯流排的probe函式(匯流排的probe函式優先順序高於總線上裝置的probe函式)即virtio_dev_probe;
  (5)virtio_dev_probe呼叫virtballoon_probe完成最後的初始化任務。

我們最終需要關注的是virtballoon_probe這個函式是怎麼被呼叫到的,linux裝置初始化開始到呼叫到virtballoon_probe的過程簡化如下,僅供參考: 驅動可執行的動作包含在virtio_balloon_driver定義的結構體中。先來看下這個結構體的內容,檔案位置driver/virtio/virtio_balloon.c。
static unsigned int features[] = {
	VIRTIO_BALLOON_F_MUST_TELL_HOST,
	VIRTIO_BALLOON_F_STATS_VQ,
	VIRTIO_BALLOON_F_DEFLATE_ON_OOM,
};

static struct virtio_driver virtio_balloon_driver = {
	.feature_table = features,
	.feature_table_size = ARRAY_SIZE(features),
	.driver.name =	KBUILD_MODNAME,
	.driver.owner =	THIS_MODULE,
	.id_table =	id_table,
	.probe =	virtballoon_probe,
	.remove =	virtballoon_remove,
	.config_changed = virtballoon_changed,
#ifdef CONFIG_PM_SLEEP
	.freeze	=	virtballoon_freeze,
	.restore =	virtballoon_restore,
#endif
};
module_virtio_driver(virtio_balloon_driver);

可以看到,註冊的 driver中註冊了feature屬性,driver的名稱和owner,驅動載入的probe解除安裝的remove,感知變化的config_changed,這三個函式做了主要的工作。 先來看下載入做了什麼工作。

static int virtballoon_probe(struct virtio_device *vdev)
{
	struct virtio_balloon *vb;
	int err;

    //device的get回撥函式,用來獲取qemu側模擬的裝置的config資料
    //回撥在virtio_pci_modern.c中註冊,原型為vp_get
	if (!vdev->config->get) {
		dev_err(&vdev->dev, "%s failure: config access disabled\n",
			__func__);
		return -EINVAL;
	}
    //申請一個virtio_balloon結構
	vdev->priv = vb = vb_dev = kmalloc(sizeof(*vb), GFP_KERNEL);
	if (!vb) {
		err = -ENOMEM;
		goto out;
	}
    //需要釋放的頁面預設為0,即gust預設保留全部頁面,不使用balloon釋放
	vb->num_pages = 0;
	mutex_init(&vb->balloon_lock);
    //初始化了兩個工作佇列,用於通知對應工作佇列有訊息到達,需要被喚醒
	init_waitqueue_head(&vb->config_change);
	init_waitqueue_head(&vb->acked);
	vb->vdev = vdev;
	vb->need_stats_update = 0;
    //嘗試申請用於balloon的頁面,如果失敗一次則增加一
    //用來記錄失敗次數,如果短時間失敗過多表明gust無多餘記憶體可提供給balloon
	vb->alloc_page_tried = 0;
    //是否停止balloon,如gustos發生了lowmemkiller即記憶體不夠gust使用,則停止balloon
	atomic_set(&vb->stop_balloon, 0);

	balloon_devinfo_init(&vb->vb_dev_info);
#ifdef CONFIG_BALLOON_COMPACTION
	vb->vb_dev_info.migratepage = virtballoon_migratepage;
#endif
    //初始化virtqueue,用於和後端裝置進行通訊
    //建立了3個queue用於ivq/dvq/svq時間的資訊傳輸
    //同時註冊了三個callback函式,用來喚醒上面寫的兩個工作佇列
	err = init_vqs(vb);
	if (err)
		goto out_free_vb;
        //向oom的notify連結串列中新增處理回撥函式,在out_of_memory函式中會呼叫
	vb->nb.notifier_call = virtballoon_oom_notify;
	vb->nb.priority = VIRTBALLOON_OOM_NOTIFY_PRIORITY;
	err = register_oom_notifier(&vb->nb);
	if (err < 0)
		goto out_oom_notify;
    //讀取裝置側config的status,檢查VIRTIO_CONFIG_S_DRIVER_OK是否置位
    //若已置位說明裝置側已經可用
	virtio_device_ready(vdev);
    //啟動vballoon執行緒,balloon主要操作在這裡完成
	vb->thread = kthread_run(balloon, vb, "vballoon");
	if (IS_ERR(vb->thread)) {
		err = PTR_ERR(vb->thread);
		goto out_del_vqs;
	}

	return 0;

out_del_vqs:
	unregister_oom_notifier(&vb->nb);
out_oom_notify:
	vdev->config->del_vqs(vdev);
out_free_vb:
	kfree(vb);
out:
	return err;
}

可以看到,這裡的主要工作有:

1. 通過init_waitqueue_head初始化了兩個工作佇列用來接收QEMU發來的notify

2. 通過init_vqs初始化了3個 virt_queue用來和qemu傳送balloon進行inflate/deflate的page地址資訊以及callback回撥

3. 啟動核心執行緒執行vballoon,執行balloon的具體操作

2.2 vballoon如何運作

static int balloon(void *_vballoon)
{
	struct virtio_balloon *vb = _vballoon;
    //註冊工作佇列的喚醒函式
	DEFINE_WAIT_FUNC(wait, woken_wake_function);

	set_freezable();
	while (!kthread_should_stop()) {
		s64 diff;

		try_to_freeze();
        //將wait新增到config_change的佇列,等待喚醒
        //喚醒操作需要virtballoon_changed處理,其註冊到了驅動的config_changed
        //qemu執行virtio_notify_config傳送notify時會被呼叫
        /*gust側喚醒佇列的呼叫棧如下
        vp_interrupt
          -> vp_config_changed
            -> virtio_config_changed
	          -> __virtio_config_changed
	            ->  drv->config_changed(virtballoon_changed)
	    */
		add_wait_queue(&vb->config_change, &wait);
		for (;;) {
            //towards_target用來計算要釋放的page數量->num_pages
			if (((diff = towards_target(vb)) != 0 &&
				vb->alloc_page_tried < 5) ||
			    vb->need_stats_update ||
				!atomic_read(&vb->stop_balloon) ||
			    kthread_should_stop() ||
			    freezing(current))
			    //需要執行balloon則退出這層迴圈
				break;
			wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
            
			vb->alloc_page_tried = 0;
			atomic_set(&vb_dev->stop_balloon, 0);
		}
        //去除等待佇列,處理時暫不接受新的balloon的notify
		remove_wait_queue(&vb->config_change, &wait);
        //更新stat資訊,在初始化時置零,在stats_request呼叫時置一,並喚醒config_change佇列
        //stats_request放入了virtqueue的callback
		if (vb->need_stats_update)
			stats_handle_request(vb);
        //diff大於零表示需要重gust申請記憶體放入balloon,釋放記憶體
        //這樣gust可用的記憶體減少,因為記憶體釋放所以host可用記憶體增多
		if (diff > 0)
			fill_balloon(vb, diff);
        //diff小於零,表示gust需要從balloon中回收記憶體
        //這樣gust可用記憶體增加,host記憶體被gust佔用則可用記憶體減少
		else if (diff < 0)
			leak_balloon(vb, -diff);
        //更新balloon中記錄的actual,重新整理balloon實際申請到或釋放掉的記憶體
		update_balloon_size(vb);

		/*
		 * For large balloon changes, we could spend a lot of time
		 * and always have work to do.  Be nice if preempt disabled.
		 */
		cond_resched();
	}
	return 0;
}

主要涉及到的處理:

1. 新增等待佇列,等待config_change被喚醒,即QEMU有執行balloon操作 2. 計算需要申請或者釋放的空間,即diff值 3. 如果需要申請或者釋放空間,則呼叫fill_balloon或者leak_balloon進行操作 4. 更新balloon實際佔用的空間,記錄到actual變數中,並通知給QEMU 計算diff值的操作如下
static inline s64 towards_target(struct virtio_balloon *vb)
{
	s64 target;
	u32 num_pages;
    //獲取最新的num_pages資料
	virtio_cread(vb->vdev, struct virtio_balloon_config, num_pages,
		     &num_pages);

	/* Legacy balloon config space is LE, unlike all other devices. */
	if (!virtio_has_feature(vb->vdev, VIRTIO_F_VERSION_1))
		num_pages = le32_to_cpu((__force __le32)num_pages);

	target = num_pages;
    //使用最新的num_pages資料和已有的資料做差
	return target - vb->num_pages;
}

2.3 balloon充氣過程

static void fill_balloon(struct virtio_balloon *vb, size_t num)
{
	struct balloon_dev_info *vb_dev_info = &vb->vb_dev_info;

	/* We can only do one array worth at a time. */
	num = min(num, ARRAY_SIZE(vb->pfns));

	mutex_lock(&vb->balloon_lock);
	for (vb->num_pfns = 0; vb->num_pfns < num;
	     vb->num_pfns += VIRTIO_BALLOON_PAGES_PER_PAGE) {
        //從gust空間申請一個頁面,並且加入到vb_dev_info->pages連結串列中
        //並標記page的mapcount和設定private標誌。這樣可以讓page不會被kernel繼續使用
		struct page *page = balloon_page_enqueue(vb_dev_info);

		if (!page) {
			dev_info_ratelimited(&vb->vdev->dev,
					     "Out of puff! Can't get %u pages\n",
					     VIRTIO_BALLOON_PAGES_PER_PAGE);
			vb->alloc_page_tried++;
			/* Sleep for at least 1/5 of a second before retry. */
			msleep(200);
			break;
		}
        //清零頁面申請失敗計數
		vb->alloc_page_tried = 0;
        //填充vb->pfns陣列對應項(不太清楚作用,需再分析)
		set_page_pfns(vb, vb->pfns + vb->num_pfns, page);
        //num_pages為通知QEMU側申請到的頁面數量
		vb->num_pages += VIRTIO_BALLOON_PAGES_PER_PAGE;
		if (!virtio_has_feature(vb->vdev,
					VIRTIO_BALLOON_F_DEFLATE_ON_OOM))
			adjust_managed_page_count(page, -1);
	}

	/* Did we get any? */
	if (vb->num_pfns != 0)
        //通過ivq佇列將申請到的頁面資訊傳送給qemu
		tell_host(vb, vb->inflate_vq);
	mutex_unlock(&vb->balloon_lock);
}

基本流程可以總結為:從gust空間申請頁面放入balloon的連結串列中,並做標記使該記憶體核心不可用,填充裝置的pfn陣列,然後通過ivq通知裝置側進行處理。

2.4 leak_balloon過程

static unsigned leak_balloon(struct virtio_balloon *vb, size_t num)
{
	unsigned num_freed_pages;
	struct page *page;
	struct balloon_dev_info *vb_dev_info = &vb->vb_dev_info;

	/* We can only do one array worth at a time. */
	num = min(num, ARRAY_SIZE(vb->pfns));

	mutex_lock(&vb->balloon_lock);
	/* We can't release more pages than taken */
	num = min(num, (size_t)vb->num_pages);
	for (vb->num_pfns = 0; vb->num_pfns < num;
	     vb->num_pfns += VIRTIO_BALLOON_PAGES_PER_PAGE) {
        //將申請到balloon的頁面釋放出來
		page = balloon_page_dequeue(vb_dev_info);
		if (!page)
			break;
        //設定pfn陣列
		set_page_pfns(vb, vb->pfns + vb->num_pfns, page);
		vb->num_pages -= VIRTIO_BALLOON_PAGES_PER_PAGE;
	}

	num_freed_pages = vb->num_pfns;
	/*
	 * Note that if
	 * virtio_has_feature(vdev, VIRTIO_BALLOON_F_MUST_TELL_HOST);
	 * is true, we *have* to do it in this order
	 */
	if (vb->num_pfns != 0)
        //使用dvq通知qemu進行處理
		tell_host(vb, vb->deflate_vq);
	release_pages_balloon(vb);
	mutex_unlock(&vb->balloon_lock);
	return num_freed_pages;
}

leak_balloon的過程和fill_balloon剛好相反,它會釋放存放在balloon的page連結串列中的page項歸還給gust,同理,這部分 記憶體會被qemu從host申請回來留給gustos備用,此時host主機的可用記憶體就減少了。