用户与unikraft的参数传递

在9pfs挂载时,qemu 会传递参数给9p

1
qemu-system-x86_64 -fsdev local,id=myid,path=guest_fs,security_model=none -device virtio-9p-pci,fsdev=myid,mount_tag=fs0 -kernel build/06-adding-filesystems_kvm-x86_64 -nographic

其中-fsdev定义配置宿主机文件系统要共享的路径,该选项与 -device 驱动程序virtio-9p-xxx(9p与总线交互的外部设备)一起使用,所以一般只应用于9pfs。

目前已知三种通用的用户向unikraft的参数传递方式:

  1. 先编写config.uk,负责menuconfig的执行工作。其中需要包含希望传递的参数的宏
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // uknfs/Config.uk
    menuconfig LIBNFS
    bool "libnfs: NFS (Network Filesystem)"
    default n
    select LIBNOLIBC if !HAVE_LIBC
    select LIBUKTIME if !HAVE_LIBC
    select LIBVFSCORE

    if LIBNFS
    config LIBNFS_AUTOMOUNT
    bool "Automatically mount a network filesystem"
    default y

    config LIBNFS_SERVADDR
    string "Default nfs server ip address"
    default ""
    help
    IP address of the nfs server you want to mount

    config LIBNFS_SERVPATH
    string "Default path of nfs server"
    default ""
    help
    Directory path of the nfs server you want to mount

    config LIBNFS_MOUNTPATH
    string "Default mount path of local unikraft"
    default "/mnt/nfs"
    help
    Where do you want to mount nfs? The default path is "/mnt/nfs"

    config LIBNFS_FLAGS
    hex "Default nfs mount flags"
    default 0x0
    help
    Mount flags.

    config LIBNFS_OPTS
    string "Default nfs mount options"
    default ""
    help
    Usually a comma-separated list of additional mount
    options that are directly interpreted by the target
    filesystem.
    endif

    执行make menuconfig后,在Library Configuration里选中uk-nfs的外部库。
    image
    这里默认会自动确认nfs自动挂载操作
    image
    输入想要挂载的nfs服务器的ip地址
    image
    输入想要挂载的nfs服务器的路径
    image
    本地挂载路径默认为mnt/nfs,但也可以根据用户情况进行修改
  2. menuconfig执行后会在内核源码的顶层目录下生成一个.config文件。该文件用来保存所有的配置项,然后回到顶层Makefile开始编译。
    1
    2
    3
    4
    5
    6
    7
    8
    // uknfs/Makefile.uk
    $(eval $(call addlib_s,libnfs,$(CONFIG_LIBNFS)))

    CINCLUDES-$(CONFIG_LIBNFS) += -I$(LIBSHFS_BASE)/include
    CXXINCLUDES-$(CONFIG_LIBNFS) += -I$(LIBSHFS_BASE)/include

    # nfs项目完成后,会将所有的.c文件放进来
    LIBNFS_SRCS-$(CONFIG_LIBNFS_AUTOMOUNT) += $(LIBNFS_BASE)/automount.c

上面输入的地址、路径等都会赋给相应的宏。随后在nfs微库的c文件中提取使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// lib/uknfs/automount.c

#ifndef CONFIG_LIBNFS_SERVADDR
static const char *servaddr = "";
#else
static const char *servaddr = CONFIG_LIBNFS_SERVADDR;
#endif

#ifndef CONFIG_LIBNFS_SERVPATH
static const char *nfs_path = "";
#else
static const char *nfs_path = CONFIG_LIBNFS_SERVPATH;
#endif

#ifndef CONFIG_LIBNFS_MOUNTPATH
static const char *mountpath = "/mnt/nfs";
#else
static const char *mountpath = CONFIG_LIBNFS_MOUNTPATH;
#endif

#ifndef CONFIG_LIBNFS_FLAGS
static const __u64 *flags = "";
#else
static const char *flags = CONFIG_LIBNFS_FLAGS;
#endif

#ifndef CONFIG_LIBNFS_OPTS
static const char *opts = "";
#else
static const char *opts = LIBNFS_OPTS;
#endif

UK_LIB_PARAM_STR(servaddr);
UK_LIB_PARAM_STR(nfs_path);
UK_LIB_PARAM_STR(mountpath);
UK_LIB_PARAM(flags, __u64);
UK_LIB_PARAM_STR(opts);

static int nfs_automount(void)
{
int error;

uk_pr_info("Mount nfs to /mnt/nfs...");

/* 逐级创建本地挂载目录,如果创建失败,说明目录已经存在 */
error = mkdir(mountpath, S_IRWXU);
if (error != 0 && errno != EEXIST) {
uk_pr_err("Failed to create /mnt/nfs: %d\n", errno);
return -1;
}

/* 创建nfs挂载数据 */
void *data = get_nfs_data(servaddr, nfs_path, opts);

/* TODO:flags的默认值问题 */
error = mount("", mountpath, "nfs", flags, data);
if (error != 0) {
uk_pr_err("Failed to mount nfs to /mnt/nfs: %d\n", errno);
return -1;
}

return 0;
}

kvm镜像的cmdline方式

以kvm下的x86启动为例:

1
qemu-system-x86_64  -append [cmdline] -netdev [网络设备参数] -device [网络设备]  -kernel [镜像] -nographic

其中-append可以用来将cmdline参数传递给guest,这里可以将nfs的挂载参数传递给unikraft

linuxu镜像的cmdline方式

以xxx镜像名xxx_linuxu-x86_64举例,在镜像后加入要传输的参数即可

1
./build/xxx_linuxu-x86_64 cmdline

参数解析过程

当unikraft镜像执行后,会首先到unikraft的入口地址处调用_libkvmplat_entry

1
2
// plat/kvm/x86/link64.lds.S
ENTRY(_libkvmplat_entry)

其中传进来的*arg参数是Multiboot的信息结构。
在进入操作系统时,qemu-kvm会将部分参数存储到EBX寄存器,EBX寄存器包含Multiboot信息结构的物理地址,引导程序通过它将重要的引导信息传递给操作系统。

1
2
3
4
5
6
7
8
9
10
11
12
// plat/kvm/x86/setup.c
void _libkvmplat_entry(void *arg)
{
struct multiboot_info *mi = (struct multiboot_info *)arg;

_mb_get_cmdline(mi);
_mb_init_mem(mi);
_mb_init_initrd(mi);

_libkvmplat_newstack(_libkvmplat_cfg.bstack.end,
_libkvmplat_entry2, 0);
}

multiboot_infocmdline用于存储qemu的-append的参数
当flag的第2位置为1时cmdline才会使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// plat/kvm/include/kvm-x86/multiboot.h
struct multiboot_info {
/* Multiboot info version number */
multiboot_uint32_t flags;

/* Available memory from BIOS */
multiboot_uint32_t mem_lower;
multiboot_uint32_t mem_upper;

/* "root" partition */
multiboot_uint32_t boot_device;

/* Kernel command line */
multiboot_uint32_t cmdline;
};

随后会通过_mb_get_cmdline将cmdline存储到setup.ccmdline全局变量中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

// plat/kvm/x86/setup.c
static char cmdline[MAX_CMDLINE_SIZE];
static inline void _mb_get_cmdline(struct multiboot_info *mi)
{
char *mi_cmdline;

if (mi->flags & MULTIBOOT_INFO_CMDLINE) {
mi_cmdline = (char *)(__u64)mi->cmdline;

if (strlen(mi_cmdline) > sizeof(cmdline) - 1)
uk_pr_err("Command line too long, truncated\n");
strncpy(cmdline, mi_cmdline,
sizeof(cmdline));
} else {
/* Use image name as cmdline to provide argv[0] */
uk_pr_debug("No command line present\n");
strncpy(cmdline, CONFIG_UK_NAME, sizeof(cmdline));
}

/* ensure null termination */
cmdline[(sizeof(cmdline) - 1)] = '\0';
}

随后_libkvmplat_entry2会调用cmdline,随后委托给ukplat_entry_argp进行参数的分解
其中,uk_argnparse会用来将cmdline的字符串的每一部分分开放入argv数组中,如path=/mnt/nfs addr=192.168.18.120则会除去空行分成两部分放入argv中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void _libkvmplat_entry2(void *arg __attribute__((unused)))
{
ukplat_entry_argp(NULL, cmdline, sizeof(cmdline));
}

// lib/ukboot/boot.c
void ukplat_entry_argp(char *arg0, char *argb, __sz argb_len)
{
static char *argv[CONFIG_LIBUKBOOT_MAXNBARGS];
int argc = 0;

if (arg0) {
argv[0] = arg0;
argc += 1;
}
if (argb && argb_len) {
argc += uk_argnparse(argb, argb_len, arg0 ? &argv[1] : &argv[0],
arg0 ? (CONFIG_LIBUKBOOT_MAXNBARGS - 1)
: CONFIG_LIBUKBOOT_MAXNBARGS);
}
ukplat_entry(argc, argv);
}

ukplat_entry会将qemu的传参传递给thread_main_argargv成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// lib/ukboot/boot.c
void ukplat_entry(int argc, char *argv[])
{
struct thread_main_arg tma;
#ifdef CONFIG_LIBUKLIBPARAM
rc = (argc > 1) ? uk_libparam_parse(argv[0], argc - 1, &argv[1]) : 0;
if (unlikely(rc < 0))
uk_pr_crit("Failed to parse the kernel argument\n");
else {
kern_args = rc;
uk_pr_info("Found %d library args\n", kern_args);
}
#endif /* CONFIG_LIBUKLIBPARAM */

tma.argc = argc - kern_args;
tma.argv = &argv[kern_args];
#if CONFIG_LIBUKSCHED
main_thread = uk_thread_create("main", main_thread_func, &tma);
if (unlikely(!main_thread))
UK_CRASH("Could not create main thread\n");
uk_sched_start(s);
#else
/* Enable interrupts before starting the application */
ukplat_lcpu_enable_irq();
main_thread_func(&tma);
#endif
}

uk_libparam_parse会对cmdline参数进行解析。分为库名解析、变量名解析、赋值三步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// lib/uklibparam/param.c
struct param_args {
/* Reference to the start of the library */
char *lib;
/* Reference to the start of the parameter */
char *param;
/* Reference to the start of the value */
char *value;
/* length of the library name */
__u32 lib_len;
/* length of the parameter */
__u32 param_len;
/* length of the value */
__u32 value_len;
};

struct uk_param {
/* The name of the param */
const char *name;
/* Type information for the param */
const __u8 param_type;
/* Type information for the variable size param */
const __u8 param_size;
/* Define a reference to location of the parameter */
__uptr addr;
};

int uk_libparam_parse(const char *progname, int argc, char **argv)
{
int keindex = 0;
int rc = 0, cnt = 0, args_read, i;
struct param_args pargs = {0};
struct uk_lib_section *section = NULL;
struct uk_param *param = NULL;
// 获取参数的数量
keindex = kernel_arg_range_fetch(argc, argv);
// 遍历每一个参数
while (cnt < keindex) {
args_read = 0;
/* Fetch the argument from the input */
rc = kernel_arg_fetch(&argv[cnt], (keindex - cnt),
&pargs, &args_read);
cnt += args_read;
/* Fetch library for the argument */
rc = kernel_lib_fetch(&pargs, &section);
/* Fetch the parameter for the argument */
rc = kernel_parse_arg(&pargs, section, &param);
// 去除参数周围的引号
rc = kernel_value_sanitize(&pargs);
// 将参数传给unikraft对应的全局变量
rc = kernel_args_set(&pargs, param);
}

/* Replacing the -- with progname */
argv[keindex] = DECONST(char *, progname);

return keindex + 1;
}

kernel_args_set首先会对参数进行相关检查,随后通过委托kernel_arg_set将值分配给用户指定的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// lib/uklibparam/param.c
static int kernel_args_set(struct param_args *pargs,
struct uk_param *param)
{
int rc = 0;
int i = 0;
char *start, *value;
int sign = (param->param_type >> PARAM_SIGN_SHIFT) & PARAM_SIGN_MASK;
int scopy = (param->param_type >> PARAM_SCOPY_SHIFT) & PARAM_SCOPY_MASK;
int param_type = (param->param_type >> PARAM_SIZE_SHIFT)
& PARAM_SIZE_MASK;
if (scopy == 1)
/* Reference the pointer instead of copying the value */
*((__uptr *)param->addr) = (__uptr) pargs->value;
else {
if (param->param_size > 1) {
/* Adding support for array */
i = 0;
value = &pargs->value[i];
while (value && i < param->param_size) {
start = value;
value = strchr(value, ARRAY_SEP);
if (value) {
*value = '\0';
/* Search from the next index */
value++;
}
rc = kernel_arg_set((void *)(param->addr +
(i * param_type)),
start, param_type, sign);
if (rc < 0)
break;
i++;
}
}
}
return rc;
}

cmdline参数规则

linuxu镜像和kvm镜像的cmdline参数传递均需要按照以下规则进行传入,以空格为间隔:
[库名].[变量名]=value [库名].[变量名]=value --

  • 库名是在Unikraft构建系统中注册的名称,如uknfs。
  • 变量名是程序中全局或静态变量的名称,如servaddr。
  • --是分隔符,后面可以跟应用程序的参数

额外需要的工作

对以上三种形式的参数传递,变量名都需要在对应的库中进行提前定义声明,随后通过libparam.h的三个接口将变量注册为参数

  • UK_LIB_PARAM(变量名, 变量类型):将指定类型的值传递给变量。
  • UK_LIB_PARAM_STR(变量名):将值以空结尾的字符串传递给变量。
  • UK_LIB_PARAM_ARR(变量名, 变量类型):将指定类型的值以空格分隔转成数组的形式传递给变量。

如:

1
2
static const *char servaddr = "";
UK_LIB_PARAM_STR(servaddr);

为了让库在执行时配置选项,它需要在配置Unikraft构建时选择uklibparam库。
同时,哪个库使用uklibaram,还应使用库的Makefile.uk中的addlib_paramprefixuklibaram库注册该库
如:

1
$(eval $(call addlib_paramprefix,libuknfs,nfs))