
鸿蒙OS的系统调用是如何实现的? | 解读鸿蒙源码 精华
本文将首先带您回顾“系统调用”的概念以及它的作用,然后从经典的Hello World开始,逐行代码层层分析——鸿蒙OS的系统调用是如何实现的。
写在前面
9月10号 华为开发者大会(HDC)上,华为向广大开发者宣布了鸿蒙2.0系统开源,源码托管在国内源码托管平台“码云”上:https://openharmony.gitee.com/
我也第一时间从码云下载了鸿蒙系统的源代码,并进行了编译和分析。当晚回看了HDC上的关于鸿蒙OS 2.0的主题演讲,个人最为好奇的是——这次开源的liteos-a内核。因为它支持了带MMU(内存管理单元)的ARM Cortex-A设备;我们知道,在带有MMU的处理器上,可以实现虚拟内存,进而实现进程之间的隔离、内核态和用户态的隔离等等这些功能。
系统调用简介
引用一张官方文档中的图片,看看liteos-a内核在整个系统中的位置。
这次开源的鸿蒙系统中同时包含了两个内核,分别是liteos-a和liteos-m,其中的liteos-m和以前开源的LiteOS相当,而liteos-a是面向应用处理器的操作系统内核,提供了更为丰富的内核功能。此前已经开源的LiteOS,只是一个实时操作系统(RTOS),它主要面向的是内存和闪存配置都比较低的微控制器。
我们先来简单回顾一下操作系统课程的一个知识点——系统调用,以及为什么会有系统调用?它的作用是什么?如果你对于这两个问题以及了然于心,可以直接跳过本段,看后面的源码分析部分。
在微控制器这样的系统资源较少的硬件系统(比如STM32、MSP430、AVR、8051)上,通常直接裸跑程序(也就是不使用任何操作系统),或者使用像FreeRTOS、Zephyr这一类的实时操作系统(RTOS)。这些实时操作系统中,应用程序和内核程序直接运行在同一个物理内存空间(因为这些设备一般没有MMU)上。而RTOS只提供了线程(或者叫任务),线程间同步、互斥等基础设施;应用程序可以直接调用内核函数(用户程序和内核程序只是逻辑上的划分,本质上并没有太大不同);一旦有一个线程发生异常,整个系统就会重启。
而在ARM Cortex-A、x86、x86-64这样的系统资源丰富的硬件系统上,SoC或CPU芯片内部一般集成了MMU,而且CPU有特权级别状态(状态寄存器的某些位)。基于特权级别状态,可以实现部分硬件相关的操作只能在内核态进行,例如访问外设等,用户态应用程序不能访问硬件设备。在这样的系统上,系统调用是用户态应用程序调用内核功能的请求入口。通俗的说,系统调用就是在有内核态和用户态隔离的操作系统上,用户态进程访问内核态资源的一种方式。
从Hello World开始
接下来,我们一起从鸿蒙系统源码分析它在liteos-a内核上是如何实现系统调用的。鸿蒙OS使用了musl libc,应用程序和系统服务都通过musl libc封装的系统调用API接口访问内核相关功能。
下面,我们就从经典的helloworld分析整个系统调用的流程。鸿蒙系统目前官方支持了三个芯片平台,分别是Hi3516DV300(双核ARM Cortex A-7 @ 900M Hz),Hi3518EV300(单核ARM Cortex A-7 @ 900MHz 内置64MB DDR2内存)和Hi3861V100(单核RISC-V @160M Hz 内置 SRAM 和 Flash)。其中Hi3516和Hi3518是带有Cortex A7内核的芯片,鸿蒙系统在这两个平台使用的内核自然是liteos-a。根据官方指导文档,我们知道这两个平台的第一个应用程序示例都是helloworld,源码路径为:applications/sample/camera/app/src/helloworld.c,除去头部注释,代码内容为:
musl libc的printf函数实现分析
文件路径:third_party/musl/src/stdio/printf.c:
我们看到了,这里使用标准库的stdout作为第一个参数调用了vfprintf,我们继续向下分析third_party/musl/src/stdio/vfprintf.c文件:
这里,我们继续关注三处带有参数f的调用:__towrite(f),printf_core(f, fmt, &ap2, nl_arg, nl_type),f->write(f, 0, 0);
其中,__towrite的实现位于third_party/musl/src/stdio/__towrite.c(可见和系统调用无关):
从内容上看,__towrite函数的作用是更新文件结构FILE的wpos、wbase、wend成员,以指向待写入实际文件的内存缓冲区域,同时将rpos、rend值为零。
printf_core的实现也位于src/stdio/vfprintf.c文件:
从注释和代码结构可以看出,这个函数实现了格式化字符串展开的主要流程,这里又调用了out和pad两个函数,从命名猜测应该分别是向内存缓冲区写入内容和填充内容的函数,它们的实现也位于vfprintf.c中:
它们又调用了__fwritex,它的实现位于third_party/musl/src/stdio/fwrite.c:
这里又出现了vfprintf中出现的f->write(f, s, i),下面我们就分析这个函数实际底是什么?
我们先找到它的定义prebuilts/lite/sysroot/usr/include/arm-liteos/bits/alltypes.h:
以及third_party/musl/src/internal/stdio_impl.h:
我们再继续寻找stdout的各个成员值是什么?
可以找到third_party/musl/src/stdio/stdout.c文件中的:
third_party/musl/src/stdio/__stdout_write.c文件中:
这段代码里调用了SYS_ioctl系统调用,但主体流程是下方的函数__stdio_write,它的实现在third_party/musl/src/stdio/__stdio_write.c文件中:
至此,我们看到了printf函数最终调用到了两个系统调用SYS_ioctl和SYS_write。
musl libc的syscall函数实现分析
在上一节中,我们看到printf最终调用到了两个长得像系统调用的函数syscall和__syscall。
系统调用宏syscall的实现
在musl代码仓(third_party/musl)下搜索:
可以找到third_party/musl/src/internal/syscall.h:
这里可以看到它们两者都是宏,而syscall调用了__syscall,而__syscall又调用了__SYSCALL_DISP,它的实现也在同一个文件中:
我们以__stdio_write中调用syscall处进行分析,即尝试展开syscall(SYS_writev, f->fd, iov, iovcnt);
先忽略最外层的 __syscall_ret,展开__SYSCALL_DISP部分:
忽略外层的__SYSCALL_CONCAT,展开__SYSCALL_NARGS_X部分:
回到 __SYSCALL_CONCAT 展开流程,
再回到__SYSCALL_DISP(__syscall, SYS_writev, f->fd, iov, iovcnt)展开流程,结果应该是:
系统调用函数__syscall3的实现
这些__syscall[1-7]的系统调用包装宏定义如下:
继续搜索发现有多出匹配,我们关注arch/arm目录下的文件,因为ARM Cortext A7是Armv7-A指令集的32位CPU(如果是Armv8-A指令集的64位CPU则对应arch/aarch64下的文件):
这段代码中还有三个宏,__ASM____R7__、__asm_syscall和R7_OPERAND:
它们有两个实现版,分别对应于编译器THUMB选项的开启和关闭。这两种选项条件下的代码流程基本一致,以下仅以未开启THUMB选项为例进行分析。这两个宏展开后的__syscall3函数内容为:
这里最后的一个内嵌汇编比较复杂,它符合如下格式(具体细节可以查阅gcc内嵌汇编文档的扩展汇编说明):
汇编模板为:"svc 0", 输出参数部分为:"=r"(r0),输出寄存器为r0 输入参数部分为:"r"(r7), "0"(r0), "r"(r1), "r"(r2),输入寄存器为r7,r0,r1,r2,("0"的含义是,这个输入寄存器必须和输出寄存器第0个位置一样) Clobber部分为:"memory"
这里我们只需要记住:系统调用号存放在r7寄存器,参数存放在r0,r1,r2,返回值最终会存放在r0中;
SVC指令,ARM Cortex A7手册 的解释为:
The SVC instruction causes a Supervisor Call exception. This provides a mechanism for unprivileged software to make a call to the operating system, or other system component that is accessible only at PL1.
翻译过来就是说
SVC指令会触发一个“特权调用”异常。这为非特权软件调用操作系统或其他只能在PL1级别访问的系统组件提供了一种机制。
详细的指令说明在
到这里,我们分析了鸿蒙系统上应用程序如何进入内核态,主要分析的是musl libc的实现。
liteos-a内核的系统调用实现分析
既然SVC能够触发一个异常,那么我们就要看看liteos-a内核是如何处理这个异常的。
ARM Cortex A7中断向量表
在ARM架构参考手册中,可以找到中断向量表的说明:
可以看到SVC中断向量的便宜地址是0x08,我们可以在kernel/liteos_a/arch/arm/arm/src/startup目录的reset_vector_mp.S文件和reset_vector_up.S文件中找到相关汇编代码:
PS: kernel/liteos_a/arch/arm/arm/src/startup目录有两个文件reset_vector_mp.S文件和reset_vector_up.S文件分别对应多核和单核编译选项:
SVC中断处理函数
上面的汇编代码中可以看到,_osExceptSwiHdl函数就是SVC异常处理函数,具体实现在kernel/liteos_a/arch/arm/arm/src/los_hw_exc.S文件中:
这段代码的注释较为清楚,可以看到,内核模式会继续调用OsKernelSVCHandler,用户模式会继续调用OsArmA32SyscallHandle函数;
OsArmA32SyscallHandle函数
我们这里分析的流程是从用户模式进入的,所以调用的是OsArmA32SyscallHandle,它的实现位于kernel/liteos_a/syscall/los_syscall.c文件:
这个函数中用到了个全局数组g_syscallHandle和g_syscallNArgs,它们的定义以及初始化函数也在同一个文件中:
其中SYSCALL_HAND_DEF宏的对齐格式我做了一点调整。
从g_syscallNArgs成员赋值以及定义的地方,能看出它的每个UINT8成员被用来存放两个系统调用的参数个数,从而实现更少的内存占用;
syscall_lookup.h文件和los_syscall.c位于同一目录,它记录了系统调用函数对照表,我们仅节取一部分:
看到这里,write系统调用的内核函数终于找到了——SysWrite。
到此,我们已经知道了liteos-a的系统调用机制是如何实现的。
liteos-a内核SysWrite的实现
SysWrite函数的实现位于kernel/liteos_a/syscall/fs_syscall.c文件:
它又调用了write?但是这一次是内核空间的write,不再是 musl libc,经过一番搜索,我们可以找到另一个文件third_party/NuttX/fs/vfs/fs_write.c中的write:
找到这段代码,我们知道了:
liteos-a的vfs是在NuttX基础上实现的,NuttX是一个开源RTOS项目;
liteos-a的TCP/IP协议栈是基于lwip的,lwip也是一个开源项目;
这段代码中的write分为两个分支,socket fd调用lwip的send,另一个分支调用file_write;
至于,file_write如何调用到存储设备驱动程序,则是更底层的实现了,本文不在继续分析。
补充说明
本文内容均是基于鸿蒙系统开源项目OpenHarmony源码静态分析所整理,没有进行实际的运行环境调试,实际执行过程可能有所差异,希望发现错误的读者及时指正。文中所有路径均为整个openharmony源码树上的相对路径(而非liteos源码相对路径)。
参考链接
1.ARM Architecture Reference Manual ® ARMv7-A and ARMv7-R edition: https://developer.arm.com/docs/ddi0406/latest
2.gcc内嵌汇编文档的扩展汇编说明:https://gcc.gnu.org/onlinedocs/gcc-9.3.0/gcc/Extended-Asm.html#Extended-Asm
3.鸿蒙官方文档“内核子系统”:https://gitee.com/openharmony/docs/blob/master/readme/%E5%86%85%E6%A0%B8%E5%AD%90%E7%B3%BB%E7%BB%9FREADME.md
4.鸿蒙官方文档“ OpenHarmony轻内核”:https://gitee.com/openharmony/docs/blob/master/kernel/Readme-CN.md
5.NuttX:https://nuttx.apache.org/
6.Lwip:https://savannah.nongnu.org/projects/lwip/
