Email: BuddyZhang1 buddy.zhang@aliyun.com

目录


通用原理


原理简介

系统调用在内核中都是必不可少的一部分,RISCV64 架构支持的系统调用达到 295 个 (linux 5.0). 由于 RISCV64 架构系统调用位于 __NR_arch_specific_syscall 之后到 260 之间的系统调用号,其系统调用入口定义在:

arch/riscv/include/uapi/asm/unistd.h

开发者向该文件添加新系统调用入口信息,接着修改:

arch/riscv/include/asm/vdso.h

向上面文件添加系统调用声明信息,就可以创建新的入口。系统调用 内核部分和用户空间部分与其他架构无差异。


内核添加系统调用过程

添加内核系统调用的接口,首先需要确定下一个可用的系统调用号, 开发者可以参考:

arch/riscv/include/uapi/asm/unistd.h

从上图可以知道,RISCV64 架构里,新增加的系统调用号与 “__NR_arch_specific_syscall” 有关,接着可以查看该宏的定义, 位于源码:

include/uapi/asm-generic/unistd.h

从上面的定义可以看出,linux 为体系预留了 244 到 260 之间的系统调用号, 因此结合上述两个信息,新系统调用的系统调用号可以取 258, 即:

syscall_nr = __NR_arch_specific_syscall + 14 = 244 + 14 = 258

接下来,在 “arch/riscv/include/uapi/asm/unistd.h” 文件中添加新的系统 调用信息:

在该文件中,首先定义了新添加系统调用号的名字为 “__NR_hello_BiscuitOS”, 其具体值为之前查找的下一个可用系统调用号 258,也可以按格式写成 “__NR_arch_specific_syscall + 14”. 接着使用宏 “__SYSCALL_()” 创建一个新的系统调用入口,该宏的第一个参数设置为新系统调用号, 即 “__NR_hello_BiscuitOS”, 该宏的第二个参数设置为系统调用内核实现, 这里填写 “sys_hello_BiscuitOS”, 该函数就是新系统调用的具体实现。 添加完上面的信息后,还需要添加函数声明,位于文件:

arch/riscv/include/asm/vdso.h

使用 asmlinkage 关键字,函数返回值类型为 long. 函数名为 “sys_hello_BiscuitOS”. 至此新系统调用的入口制作完成。接下来, 开发者可以在内核源码的任意位置添加 sys_hello_BiscuitOS 的具体 实现。例如在源码 “fs” 目录下,创建一个名为 BiscuitOS_syscall.c 的文件,文件内容如下:

接着修改内核源码 “fs/Kconfig” 文件,添加如下内容:

接着修改内核源码 “fs/Makefile” 文件,添加内容如下:

接着是配置内核,将 BiscuitOS_syscall.c 文件加入内核编译树,如下:

cd linux_src/
make menuconfig ARCH=riscv

选择并进入 “File systems —>”

选择 “[*] BiscuitOS syscall hello” 并保存内核配置。

接着重新编译内核。编译内核中会打印相关的信息如下图:

从上面的编译信息可以看出,之前的修改已经生效。编译系统调用相关的脚本 自动为hello_BiscuitOS 生成了相关的系统调用,


用户空间调用新系统调用

调用新系统调用的最后就是在用户空间添加一个系统调用的函数,如下:

用户空间可以通过 “syscall()” 函数调用系统调用。对用户空间的程序编译之后在 RISCV64 的 Linux 上运行情况如下:


添加零参数的系统调用


实践原理

零参数的系统调用即用户空间不传递参数,直接调用系统调用。这类系统调用很 常见,比如 “sys_getgroups()” 、”sys_getppid()” 等,这类系统在内核的实现 使用 “SYSCALL_DEFINE0()” 进行定义,例如:

SYSCALL_DEFINE0(getppid)
{
        int pid;

        rcu_read_lock();
        pid = task_tgid_vnr(rcu_dereference(current->real_parent));
        rcu_read_unlock();

        return pid;
}

用户空间调用零参数的系统调用,只需向 “syscall()” 函数传递系统调用号, 例如:

int main(void)
{
	pid_t pid;

	pid = syscall(__NR_getppid);
	return 0;
}

对于零参数系统调用的返回值,返回的数据类型与传入参数无关,因此开发者 可以根据需求自行定义返回的数据。


实践准备

本实践基于 RISCV64 架构,因此在实践之前需要准备一个 RISCV64 架构的运行 平台,开发者可以在 BiscuitOS 进行实践,如果还没有搭建 BiscuitOS RISCV64 实践环境的开发者,可以参考如下文档进行搭建:

开发环境搭建完毕之后,可以继续下面的内容,如果开发者不想采用 BiscuitOS 提供的开发环境,可以继续参考下面的内容在开发者使用 的环境中进行实践。(推荐使用 BiscuitOS 开发环境)。搭建完毕之后, 使用如下命令:

cd BiscuitOS/
make linux-5.0-riscv64_defconfig
make

上图显示了 RISCV64 实践环境的位置,以及相关的 README.md 文档,开发者 可以参考 README.md 的内容搭建一个运行在 QEMU 上的 RISCV64 Linux 开发 环境:


添加用户空间实现

BiscuitOS 提供了一套完整的系统调用编译系统,开发者可以使用下面步骤部署一个 简单的用户空间调用接口文件,BiscuitOS 并可以对该文件进行交叉编译,安装, 打包和目标系统上运行的功能,节省了很多开发时间。如果开发者不想使用这套 编译机制,可以参考下面的内容进行移植。开发者首先获得用户空间系统调用 基础源码,如下:

cd BiscuitOS
make linux-5.0-riscv64_defconfig
make menuconfig

选择并进入 “[*] Package —>”

选择 “[*] strace” 和 “[*] System Call” 并进入 “[*] System Call —>”

选择并进入 “[*] sys_hello_BiscuitOS —>”

选择 “[*] SYSCALL_DEFINE0(): Zero Paramenter —>” 保存配置并退出. 接下来执行下面的命令部署用户空间系统调用程序部署:

cd BiscuitOS
make

执行完毕后,终端输出相关的信息, 接下来进入源码位置,使用如下命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE0_common-0.0.1

这个目录就是用于部署用户空间系统调用程序,开发者继续使用命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE0_common-0.0.1
make prepare
make download

执行上面的命令之后,BiscuitOS 自动部署了程序所需的所有文件,如下:

tree

上图中,main.c 与用户空间系统调用相关的源码, “SYSCALL_DEFINE0_common-0.0.1/Makefile” 是 main.c 交叉编译的逻辑。 “SYSCALL_DEFINE0_common-0.0.1/BiscuitOS_syscall.c” 文件是新系统调用 内核实现。因此对于用户空间的系统调用,开发者只需关注 main.c, 内容如下:

根据在内核中创建的入口,这里定义了入口宏的值为 258,一定要与内核定义 的入口值相呼应。由于是无参数的系统调用,因此直接使用 “syscall()” 函数, 只需要传入调用号即可, 源码准备好之后,接下来是交叉编译源码并打包到 rootfs 里,使用如下命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE0_common-0.0.1
make
make install
make pack


添加内核系统调用入口

添加内核系统调用的接口,首先需要确定下一个可用的系统调用号, 开发者可以参考:

arch/riscv/include/uapi/asm/unistd.h

从上图可以知道,RISCV64 架构里,新增加的系统调用号与 “__NR_arch_specific_syscall” 有关,接着可以查看该宏的定义, 位于源码:

include/uapi/asm-generic/unistd.h

从上面的定义可以看出,linux 为体系预留了 244 到 260 之间的系统调用号, 因此结合上述两个信息,新系统调用的系统调用号可以取 258, 即:

syscall_nr = __NR_arch_specific_syscall + 14 = 244 + 14 = 258

接下来,在 “arch/riscv/include/uapi/asm/unistd.h” 文件中添加新的系统 调用信息:

在该文件中,首先定义了新添加系统调用号的名字为 “__NR_hello_BiscuitOS”, 其具体值为之前查找的下一个可用系统调用号 258,也可以按格式写成 “__NR_arch_specific_syscall + 14”. 接着使用宏 “__SYSCALL_()” 创建一个新的系统调用入口,该宏的第一个参数设置为新系统调用号, 即 “__NR_hello_BiscuitOS”, 该宏的第二个参数设置为系统调用内核实现, 这里填写 “sys_hello_BiscuitOS”, 该函数就是新系统调用的具体实现。 添加完上面的信息后,还需要添加函数声明,位于文件:

arch/riscv/include/asm/vdso.h

使用 asmlinkage 关键字,函数返回值类型为 long. 函数名为 “sys_hello_BiscuitOS”. 至此新系统调用的入口制作完成。接下来, 开发者可以在内核源码的任意位置添加 sys_hello_BiscuitOS 的具体 实现.


添加内核实现

添加完系统号之后,需要在内核中添加系统调用的具体实现。开发者 可以参考下面的例子进行添加。本例子在内核源码 “fs/” 目录下添加 一个名为 BiscuitOS_syscall.c 的文件,如下:

cp -rfa BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE0_common-0.0.1/BiscuitOS_syscall.c  BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs/
cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi BiscuitOS_syscall.c

接着修改内核源码 “fs/Kconfig” 文件,添加如下内容:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi Kconfig

接着修改内核源码 “fs/Makefile” 文件,添加内容如下:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi Makefile

接着是配置内核,将 BiscuitOS_syscall.c 文件加入内核编译树,如下:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/
make menuconfig ARCH=riscv

选择并进入 “File systems —>”

选择 “[*] BiscuitOS syscall hello” 并保存内核配置。

接着重新编译内核。

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/
make ARCH=riscv CROSS_COMPILE=BiscuitOS/output/linux-5.0-riscv64/riscv64-biscuitos-linux-gnu/riscv64-biscuitos-linux-gnu/bin/riscv64-biscuitos-linux-gnu- vmlinux -j4

编译内核中会打印相关的信息如下图:

从上面的编译信息可以看出,之前的修改已经生效。编译系统调用相关的脚本 自动为hello_BiscuitOS 生成了相关的系统调用,


运行系统调用

在一切准备好之后,最后一步就是在 RISCV64 上运行系统调用,参考下面 命令进行运行:

cd BiscuitOS/output/linux-5.0-riscv64/
./RunBiscuitOS.sh

从运行结果可以看到,用户空间的程序已经调用到对应的内核系统调用了。此时 可以使用 strace 工具查看具体的系统调用过程,如下:

~ #
~ # strace SYSCALL_DEFINE0_common-0.0.1

从 strace 打印的消息可以看出 “syscall_0x102()” 正好程序里产生的系统调用.


添加一个或多个参数的系统调用


实践原理

一个或多个参数的系统调用即用户空间传递一个或多个参数来调用系统调用。 这类系统调用很常见,比如 “sys_times()” 、”sys_getpgid()” 等, 这类系统在内核的实现使用 “SYSCALL_DEFINE1()”、”SYSCALL_DEFINE2()” 等宏进行定义,例如:

SYSCALL_DEFINE1(getpgid, pid_t, pid)
{
        return do_getpgid(pid);
}

SYSCALL_DEFINE2(utime, char __user *, filename, struct utimbuf __user *, times)
{
        struct timespec64 tv[2];

        if (times) {
                if (get_user(tv[0].tv_sec, &times->actime) ||
                    get_user(tv[1].tv_sec, &times->modtime))
                        return -EFAULT;
                tv[0].tv_nsec = 0;
                tv[1].tv_nsec = 0;
        }
        return do_utimes(AT_FDCWD, filename, times ? tv : NULL, 0);
}

用户空间调用一个参数的系统调用,需向 “syscall()” 函数传递系统调用号, 以及一个参数,例如:

int main(void)
{
	pid_t pid = 978, current_pid;
	char *filename = "/BiscuitOS_file";
	struct utimebuf buf;

	current_pid = syscall(__NR_getpgid, pid);

	syscall(__NR_utime, filename, &buf);
	return 0;
}

对于传入多个参数的系统调用以及参数类型,开发者可以参考如下文档:

对于一个或多个参数系统调用的返回值,返回的数据类型与传入参数无关, 因此开发者可以根据需求自行定义返回的数据.


实践准备

本实践基于 RISCV64 架构,因此在实践之前需要准备一个 RISCV64 架构的运行 平台,开发者可以在 BiscuitOS 进行实践,如果还没有搭建 BiscuitOS RISCV64 实践环境的开发者,可以参考如下文档进行搭建:

开发环境搭建完毕之后,可以继续下面的内容,如果开发者不想采用 BiscuitOS 提供的开发环境,可以继续参考下面的内容在开发者使用 的环境中进行实践。(推荐使用 BiscuitOS 开发环境)。搭建完毕之后, 使用如下命令:

cd BiscuitOS/
make linux-5.0-riscv64_defconfig
make

上图显示了 RISCV64 实践环境的位置,以及相关的 README.md 文档,开发者 可以参考 README.md 的内容搭建一个运行在 QEMU 上的 RISCV64 Linux 开发 环境:


添加用户空间实现

BiscuitOS 提供了一套完整的系统调用编译系统,开发者可以使用下面步骤部署一个 简单的用户空间调用接口文件,BiscuitOS 并可以对该文件进行交叉编译,安装, 打包和目标系统上运行的功能,节省了很多开发时间。如果开发者不想使用这套 编译机制,可以参考下面的内容进行移植。开发者首先获得用户空间系统调用 基础源码,如下:

cd BiscuitOS
make linux-5.0-riscv64_defconfig
make menuconfig

选择并进入 “[*] Package —>”

选择 “[*] strace” 和 “[*] System Call” 并进入 “[*] System Call —>”

选择并进入 “[*] sys_hello_BiscuitOS —>”

选择 “[*] SYSCALL_DEFINE1(): One Paramenter —>” 保存配置并退出. 接下 来执行下面的命令部署用户空间系统调用程序部署:

cd BiscuitOS
make

执行完毕后,终端输出相关的信息, 接下来进入源码位置,使用如下命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE1_common-0.0.1

这个目录就是用于部署用户空间系统调用程序,开发者继续使用命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE1_common-0.0.1
make prepare
make download

执行上面的命令之后,BiscuitOS 自动部署了程序所需的所有文件,如下:

tree

上图中,main.c 与用户空间系统调用相关的源码, “SYSCALL_DEFINE1_common-0.0.1/Makefile” 是 main.c 交叉编译的逻辑。 “SYSCALL_DEFINE1_common-0.0.1/BiscuitOS_syscall.c” 是系统调用的内核实现。 因此开发者只需关注 main.c, 内容如下:

根据在内核中创建的入口,这里定义了入口宏的值为 258,一定要与内核定义 的入口值相呼应。由于是多个参数的系统调用,因此直接使用 “syscall()” 函数, 传入调用号和多个参数, 用户空间传递字符串 “BiscuitOS_Userpsace” 到内核 空间,并期待内核空间传递字符串到用户空间,如果成功,则打印字符串. 源码准备好之后,接下来是交叉编译源码并打包到 rootfs 里, 使用如下命令:

cd BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE1_common-0.0.1
make
make install
make pack


添加内核系统调用入口

添加内核系统调用的接口,首先需要确定下一个可用的系统调用号, 开发者可以参考:

arch/riscv/include/uapi/asm/unistd.h

从上图可以知道,RISCV64 架构里,新增加的系统调用号与 “__NR_arch_specific_syscall” 有关,接着可以查看该宏的定义, 位于源码:

include/uapi/asm-generic/unistd.h

从上面的定义可以看出,linux 为体系预留了 244 到 260 之间的系统调用号, 因此结合上述两个信息,新系统调用的系统调用号可以取 258, 即:

syscall_nr = __NR_arch_specific_syscall + 14 = 244 + 14 = 258

接下来,在 “arch/riscv/include/uapi/asm/unistd.h” 文件中添加新的系统 调用信息:

在该文件中,首先定义了新添加系统调用号的名字为 “__NR_hello_BiscuitOS”, 其具体值为之前查找的下一个可用系统调用号 258,也可以按格式写成 “__NR_arch_specific_syscall + 14”. 接着使用宏 “__SYSCALL_()” 创建一个新的系统调用入口,该宏的第一个参数设置为新系统调用号, 即 “__NR_hello_BiscuitOS”, 该宏的第二个参数设置为系统调用内核实现, 这里填写 “sys_hello_BiscuitOS”, 该函数就是新系统调用的具体实现。 添加完上面的信息后,还需要添加函数声明,位于文件:

arch/riscv/include/asm/vdso.h

使用 asmlinkage 关键字,函数返回值类型为 long. 函数名为 “sys_hello_BiscuitOS”. 至此新系统调用的入口制作完成。接下来, 开发者可以在内核源码的任意位置添加 sys_hello_BiscuitOS 的具体 实现.


添加内核实现

添加完系统号之后,需要在内核中添加系统调用的具体实现。开发者 可以参考下面的例子进行添加。本例子在内核源码 “fs/” 目录下添加 一个名为 BiscuitOS_syscall.c 的文件,如下:

cp -rfa BiscuitOS/output/linux-5.0-riscv64/package/SYSCALL_DEFINE1_common-0.0.1/BiscuitOS_syscall.c  BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs/
cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi BiscuitOS_syscall.c

内核使用 SYSCALL_DEFINE1() 宏定义了内核实现的接口函数,其包含一个 来自用户空间的字符串参数。内核使用 “copy_from_user()” 将用户空间的 字符串拷贝到内核空间,并打印字符串,接着用使用 “copy_to_user()” 函数 将内核字符串 kstring 拷贝到用户空间。这样就实现用户空间和内核空间互相 交换数据。接着修改内核源码 “fs/Kconfig” 文件,添加如下内容:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi Kconfig

接着修改内核源码 “fs/Makefile” 文件,添加内容如下:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/fs
vi Makefile

接着是配置内核,将 BiscuitOS_syscall.c 文件加入内核编译树,如下:

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/
make menuconfig ARCH=riscv

选择并进入 “File systems —>”

选择 “[*] BiscuitOS syscall hello” 并保存内核配置。

接着重新编译内核。

cd BiscuitOS/output/linux-5.0-riscv64/linux/linux/
make ARCH=riscv CROSS_COMPILE=BiscuitOS/output/linux-5.0-riscv64/riscv64-biscuitos-linux-gnu/riscv64-biscuitos-linux-gnu/bin/riscv64-biscuitos-linux-gnu- vmlinux -j4

编译内核中会打印相关的信息如下图:

从上面的编译信息可以看出,之前的修改已经生效。编译系统调用相关的脚本 自动为hello_BiscuitOS 生成了相关的系统调用,


运行系统调用

在一切准备好之后,最后一步就是在 RISCV64 上运行系统调用,参考下面 命令进行运行:

cd BiscuitOS/output/linux-5.0-riscv64/
./RunBiscuitOS.sh

从运行结果可以看到,用户空间的程序已经调用到对应的内核系统调用了。此时 可以使用 strace 工具查看具体的系统调用过程,如下:

~ #
~ # strace SYSCALL_DEFINE1_common-0.0.1

从 strace 打印的消息可以看出 “syscall_0x102()” 正好程序里产生的系统调用.


附录

BiscuitOS Home

BiscuitOS Driver

Linux Kernel

Bootlin: Elixir Cross Referencer

捐赠一下吧 🙂

MMU