本文属于《 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_async和bf_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。
以上内容,主要是完成配置、资源的预加载。
下一节,我们看看虚拟机的初始化。