TinyEMU源码分析之配置加载


本文属于《 TinyEMU模拟器基础系列教程》之一,欢迎查看其它文章。
本文中使用的代码,均为伪代码,删除了部分源码。

1 程序主流程

TinyEMU的main入口函数,位于temu.c中。
我们梳理了,该函数中主要的功能代码(只保留了重要的),如下所示:

int main(int argc, char **argv)
{
    VirtMachine *s;
    VirtMachineParams p_s, *p = &p_s;

	// 处理TinyEMU输入参数
    for(;;) {
        c = getopt_long_only(argc, argv, "hm:", options, &option_index);
        if (c == -1)
            break;
        switch(c) {
        case 0:
            break;
        case 'h':
            help();
            break;
        }
    }

	// 加载配置文件xxx.cfg
    virt_machine_load_config_file(p, path, NULL, NULL);

	// 初始化设备
    for(i = 0; i < p->drive_count; i++) {
    }
    for(i = 0; i < p->fs_count; i++) {
    }
    for(i = 0; i < p->eth_count; i++) {
    }
    if (p->display_device) {
        sdl_init(p->width, p->height);
    }

	// 初始化虚拟机
    s = virt_machine_init(p);
    virt_machine_free_config(p); // 释放配置文件资源
    
    for(;;) {
		// 启动虚拟机运行
        virt_machine_run(s); 
    }
	
	// 退出虚拟机,并释放资源
    virt_machine_end(s); 
    return 0;
}

处理TinyEMU输入参数,诸如-h、-ctrlc等启动参数。

主流程比较简单,主要是加载配置文件,初始化一些设备,以及虚拟机的初始化、运行、结束。

2 加载配置文件

配置文件加载,主要在virt_machine_load_config_file函数中完成。

void virt_machine_load_config_file(VirtMachineParams *p,
                                   const char *filename,
                                   void (*start_cb)(void *opaque),
                                   void *opaque)
{
    VMConfigLoadState *s;
    config_load_file(s, filename, config_file_loaded, s);
}
static void config_load_file(VMConfigLoadState *s, const char *filename,
                             FSLoadFileCB *cb, void *opaque)
{
    //    printf("loading %s\n", filename);
 	uint8_t *buf;
    int size;
    // 加载文件
    size = load_file(&buf, filename);
    cb(opaque, buf, size);
    free(buf);
}
static void config_file_loaded(void *opaque, uint8_t *buf, int buf_len)
{
	// 解析cfg文件
    if (virt_machine_parse_config(p, (char *)buf, buf_len) < 0)
        exit(1);
    /* load the additional files */
    s->file_index = 0;
    config_additional_file_load(s);
}

先通过公共的load_file函数,将配置文件(这里是root-riscv64.cfg),加载到内存,并返回内存地址(buf)。

然后,通过virt_machine_parse_config函数,解析配置文件buf中各个key,并将对应的value,初始化到VirtMachineParams对象中。其大致形式如下:

VirtMachineParams *p
p->machine_name = strdup(str);
p->vmc = virt_machine_find_class(p->machine_name);
p->ram_size = (uint64_t)val << 20;
p->files[VM_FILE_BIOS].filename = strdup(bios);
p->files[VM_FILE_KERNEL].filename = strdup(kernel);
p->files[VM_FILE_INITRD].filename = strdup(initrd);
p->cmdline = cmdline_subst(cmdline);
p->tab_drive[p->drive_count].filename = strdup(drive%d);

3 加载BIOS和Kernel

解析完配置文件后,通过config_additional_file_load函数,加载bbl64.bin、kernel-riscv64.bin文件。

static void config_additional_file_load(VMConfigLoadState *s)
{
    VirtMachineParams *p = s->vm_params;
    while (s->file_index < VM_FILE_COUNT &&
           p->files[s->file_index].filename == NULL) {
        s->file_index++;
    }
    if (s->file_index == VM_FILE_COUNT) {
        if (s->start_cb)
            s->start_cb(s->opaque);
        free(s);
    } else {
        char *fname;
        
        fname = get_file_path(p->cfg_filename,
                              p->files[s->file_index].filename);
        config_load_file(s, fname,
                         config_additional_file_load_cb, s);
        free(fname);
    }
}

它会根据s->file_index(从0开始),去取p->files[]数组中的元素,每个元素结构如下:

typedef struct {
	char* filename; // 文件名
	uint8_t* buf;	// 文件内容
	int len;		// 文件长度
} VMFileEntry;

然后,通过get_file_path函数,获取文件名;
最后,再次通过上面的config_load_file函数,加载文件。
利用传入函数指针的方式,会多次调用config_additional_file_load函数,直到将files[]数组中,所有文件全部遍历一遍。

VM_FILE_BIOS=0
VM_FILE_KERNEL=2
VM_FILE_COUNT=4
第一轮,s->file_index=0,加载bbl64.bin,到p->files[VM_FILE_BIOS].buf。
第二轮,s->file_index=2,加载kernel-riscv64.bin,到p->files[VM_FILE_KERNEL].buf。

当把所有文件加载完毕后,实际上就是用文件内容,将p->files[]数组,进行了数据填充

但是这里,只会加载BIOS和Kernel,不会加载文件系统root-riscv64.bin。

4 加载文件系统

在加载配置文件时,会通过“drive*”字段,来计算有多少个drive,从而初始化p->drive_count,我们例子中,这里只有一个drive0。

drive0: { file: "root-riscv64.bin" }

加载文件系统(root-riscv64.bin),在主流程的drive_count循环中完成。

for(i = 0; i < p->drive_count; i++) {
	fname = get_file_path(p->cfg_filename, p->tab_drive[i].filename);
	drive = block_device_init(fname, drive_mode);
	p->tab_drive[i].block_dev = drive;
}

通过get_file_path函数,获取文件名;
block_device_init函数中,完成文件加载,加载结果,是返回一个BlockDevice对象,结构如下:

struct BlockDevice {
    int64_t (*get_sector_count)(BlockDevice *bs);
    int (*read_async)(BlockDevice *bs,
                      uint64_t sector_num, uint8_t *buf, int n,
                      BlockDeviceCompletionFunc *cb, void *opaque);
    int (*write_async)(BlockDevice *bs,
                       uint64_t sector_num, const uint8_t *buf, int n,
                       BlockDeviceCompletionFunc *cb, void *opaque);
    void *opaque;
};

并将该结果,保存到p->tab_drive[i].block_dev中。
block_device_init函数,如下:

static BlockDevice *block_device_init(const char *filename,
                                      BlockDeviceModeEnum mode)
{
    BlockDevice *bs;
    BlockDeviceFile *bf;
    int64_t file_size;
    FILE *f;

    f = fopen(filename, mode_str);
    fseek(f, 0, SEEK_END);
    file_size = ftello(f);
    bs = mallocz(sizeof(*bs));
    bf = mallocz(sizeof(*bf));

    bf->mode = mode;
    bf->nb_sectors = file_size / 512;
    bf->f = f;
    bs->opaque = bf;
    bs->get_sector_count = bf_get_sector_count;
    bs->read_async = bf_read_async;
    bs->write_async = bf_write_async;
    return bs;
}

block_device_init函数,主要作用是,将root-riscv64.bin打开,获取FILE指针、大小,用于初始化BlockDeviceFile;
然后,再将BlockDeviceFile(bf),以及一些函数指针(bf_get_sector_count、bf_read_async、bf_write_async),用于初始化BlockDevice(bs)。

可以发现,此处并没有对该文件,进行读写,就是简单的,用文件信息,初始化BlockDevice对象。

那么,这些指针,是什么用处呢?

bf_read_asyncbf_write_async函数,实现如下:

static int bf_read_async(BlockDevice *bs,
                         uint64_t sector_num, uint8_t *buf, int n,
                         BlockDeviceCompletionFunc *cb, void *opaque)
{
    BlockDeviceFile *bf = bs->opaque;
	for(i = 0; i < n; i++) {
		if (!bf->sector_table[sector_num]) {
			fseek(bf->f, sector_num * SECTOR_SIZE, SEEK_SET);
			fread(buf, 1, SECTOR_SIZE, bf->f);
		} else {
			memcpy(buf, bf->sector_table[sector_num], SECTOR_SIZE);
		}
		sector_num++;
		buf += SECTOR_SIZE;
	}
    /* synchronous read */
    return 0;
}

static int bf_write_async(BlockDevice *bs,
                          uint64_t sector_num, const uint8_t *buf, int n,
                          BlockDeviceCompletionFunc *cb, void *opaque)
{
    BlockDeviceFile *bf = bs->opaque;
    switch(bf->mode) {
    case BF_MODE_RW: // 读写模式(写入文件)
        fseek(bf->f, sector_num * SECTOR_SIZE, SEEK_SET);
        fwrite(buf, 1, n * SECTOR_SIZE, bf->f);
        break;
    case BF_MODE_SNAPSHOT: // 快照模式(不写入文件)
        {
            int i;
            if ((sector_num + n) > bf->nb_sectors)
                return -1;
            for(i = 0; i < n; i++) {
                if (!bf->sector_table[sector_num]) {
                    bf->sector_table[sector_num] = malloc(SECTOR_SIZE);
                }
                memcpy(bf->sector_table[sector_num], buf, SECTOR_SIZE);
                sector_num++;
                buf += SECTOR_SIZE;
            }
            ret = 0;
        }
        break;
    }
}

大致可以理解为,当执行load/store相关访问指令时,便会对文件系统进行访问。

  • 比如,当执行load时,就会调用bf_read_async函数,以便从文件root-riscv64.bin中读取数据。

在读取时,其实就是把之前保存的FILE*文件指针,取出来,对文件进行读取。

  • 当执行store时,就会调用bf_write_async函数,以便将数据,写入root-riscv64.bin中。

在写入时,默认为快照模式,也就是对文件系统的写入,全部保存到内存,不保存到文件中。如果为读写模式,则对文件系统的写入,将被保存到文件中。
具体是哪个模式,可以通过TinyEMU的启动参数,来指定:

  • 若为“-rw”,表示读写模式;
  • 若不指定,表示快照模式。

5 加载网络设备

for(i = 0; i < p->eth_count; i++) {
	...
}

其实就是填充EthernetDevice对象,这个对象最后,被保存到 p->tab_eth[i].net。

6 初始化终端

p->console = console_init(allow_ctrlc);

其实就是填充CharacterDevice对象,这个对象最后,被保存到p->console。

以上内容,主要是完成配置、资源的预加载。
下一节,我们看看虚拟机的初始化。

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百里杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值