鸿蒙内核源码分析(系统调用篇) | 图+代码详细追踪鸿蒙系统调用整个过程 | 中文注解HarmonyOS源码 | v37.01
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >
本篇说清楚系统调用
读本篇之前建议先读鸿蒙内核源码分析(总目录)工作模式篇.
本篇通过一张图和七段代码详细说明系统调用的整个过程,代码一捅到底,直到汇编层再也捅不下去. 先看图,这里的模式可以理解为空间,因为模式不同运行的栈空间就不一样.
过程解读
- 在应用层
main
中使用系统调用mq_open
(posix标准接口) mq_open
被封装在库中,这里直接看库里的代码.mq_open
中调用syscall
,将参数传给寄出器R7,R0~R6
SVC 0
完成用户模式到内核模式(SVC)的切换_osExceptSwiHdl
运行在svc模式下.- PC寄存器直接指向
_osExceptSwiHdl
处取指令. _osExceptSwiHdl
是汇编代码,先保存用户模式现场(R0~R12寄存器),并调用OsArmA32SyscallHandle
完成系统调用OsArmA32SyscallHandle
中通过系统调用号(保存在R7寄存器)查询对应的注册函数SYS_mq_open
SYS_mq_open
是本次系统调用的实现函数,完成后return回到OsArmA32SyscallHandle
OsArmA32SyscallHandle
再return回到_osExceptSwiHdl
_osExceptSwiHdl
恢复用户模式现场(R0~R12寄存器)- 从内核模式(SVC)切回到用户模式,PC寄存器也切回用户现场.
- 由此完成整个系统调用全过程
七段追踪代码,逐个分析
1.应用程序 main
int main(void) { char mqname[NAMESIZE], msgrv1[BUFFER], msgrv2[BUFFER]; const char *msgptr1 = "test message1"; const char *msgptr2 = "test message2 with differnet length"; mqd_t mqdes; int prio1 = 1, prio2 = 2; struct timespec ts; struct mq_attr attr; int unresolved = 0, failure = 0; sprintf(mqname, "/" FUNCTION "_" TEST "_%d", getpid()); attr.mq_msgsize = BUFFER; attr.mq_maxmsg = BUFFER; mqdes = mq_open(mqname, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); if (mqdes == (mqd_t)-1) { perror(ERROR_PREFIX "mq_open"); unresolved = 1; } if (mq_send(mqdes, msgptr1, strlen(msgptr1), prio1) != 0) { perror(ERROR_PREFIX "mq_send"); unresolved = 1; } printf("Test PASSED\n"); return PTS_PASS; }
2. mq_open 发起系统调用
mqd_t mq_open(const char *name, int flags, ...) { mode_t mode = 0; struct mq_attr *attr = 0; if (*name == '/') name++; if (flags & O_CREAT) { va_list ap; va_start(ap, flags); mode = va_arg(ap, mode_t); attr = va_arg(ap, struct mq_attr *); va_end(ap); } return syscall(SYS_mq_open, name, flags, mode, attr); }
解读
SYS_mq_open
是真正的系统调用函数,对应一个系统调用号__NR_mq_open
,通过宏SYSCALL_HAND_DE
F将SysMqOpen
注册到g_syscallHandle
中.
static UINTPTR g_syscallHandle[SYS_CALL_NUM] = {0}; //系统调用入口函数注册 static UINT8 g_syscallNArgs[(SYS_CALL_NUM + 1) / NARG_PER_BYTE] = {0};//保存系统调用对应的参数数量 #define SYSCALL_HAND_DEF(id, fun, rType, nArg) \ if ((id) < SYS_CALL_NUM) { \ g_syscallHandle[(id)] = (UINTPTR)(fun); \ g_syscallNArgs[(id) / NARG_PER_BYTE] |= ((id) & 1) ? (nArg) << NARG_BITS : (nArg); \ } \ #include "syscall_lookup.h" #undef SYSCALL_HAND_DEF SYSCALL_HAND_DEF(__NR_mq_open, SysMqOpen, mqd_t, ARG_NUM_4)
g_syscallNArgs
为注册函数的参数个数,也会一块记录下来.- 四个参数为 SYS_mq_open的四个参数,后续将保存在R0~R3寄存器中
3. syscall
long syscall(long n, ...) { va_list ap; syscall_arg_t a,b,c,d,e,f; va_start(ap, n); a=va_arg(ap, syscall_arg_t); b=va_arg(ap, syscall_arg_t); c=va_arg(ap, syscall_arg_t); d=va_arg(ap, syscall_arg_t); e=va_arg(ap, syscall_arg_t); f=va_arg(ap, syscall_arg_t);//最多6个参数 va_end(ap); return __syscall_ret(__syscall(n,a,b,c,d,e,f)); } static inline long __syscall4(long n, long a, long b, long c, long d) { register long a7 __asm__("a7") = n; //系统调用号 R7寄存器 register long a0 __asm__("a0") = a; //R0 register long a1 __asm__("a1") = b; //R1 register long a2 __asm__("a2") = c; //R2 register long a3 __asm__("a3") = d; //R3 __asm_syscall("r"(a7), "0"(a0), "r"(a1), "r"(a2), "r"(a3)) }
解读
- 可变参数实现所有系统调用的参数的管理,可以看出,在鸿蒙内核中系统调用的参数最多不能大于6个
- R7寄存器保存了系统调用号,R0~R5保存具体每个参数
- 可变参数的具体实现后续有其余篇幅详细介绍,敬请关注.
4. svc 0
//切到SVC模式 #define __asm_syscall(...) do { \ __asm__ __volatile__ ( "svc 0" \ : "=r"(x0) : __VA_ARGS__ : "memory", "cc"); \ return x0; \ } while (0)
b reset_vector @开机代码 b _osExceptUndefInstrHdl @异常处理之CPU碰到不认识的指令 b _osExceptSwiHdl @异常处理之:软中断 b _osExceptPrefetchAbortHdl @异常处理之:取指异常 b _osExceptDataAbortHdl @异常处理之:数据异常 b _osExceptAddrAbortHdl @异常处理之:地址异常 b OsIrqHandler @异常处理之:硬中断 b _osExceptFiqHdl @异常处理之:快中断
解读
svc
全称是 SuperVisor Call,完成工作模式的切换.不管之前是7个模式中的哪个模式,统一都切到SVC管理模式.- 而软中断对应的处理函数为
_osExceptSwiHdl
,即PC寄存器将跳到_osExceptSwiHdl
执行
5. _osExceptSwiHdl
@ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理 SUB SP, SP, #(4 * 16) @先申请16个栈空间用于处理本次软中断 STMIA SP, {R0-R12} @保存R0-R12寄存器值 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 保存用户模式下的SP和LR寄存器 SUB SP, SP, #4 PUSH_FPU_REGS R1 @保存中断模式(用户模式模式) MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用*/ CPSID I @执行后续指令前必须先关中断 POP_FPU_REGS R1 @弹出FP值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行
解读
- 运行到此处,已经切到SVC的栈运行,所以先保存上一个模式的现场
- 获取中断模式,软中断的来源可不一定是用户模式,完全有可能是SVC本身,比如系统调用中又发生系统调用.就变成了从SVC模式切到SVC的模式
MOV R0, SP
;sp将作为参数传递给OsArmA32SyscallHandle
- 调用
OsArmA32SyscallHandle
这是所有系统调用的统一入口 - 注意看
OsArmA32SyscallHandle
的参数UINT32 *regs
6. OsArmA32SyscallHandle
LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } if (cmd == __NR_sigreturn) {//此时运行在内核栈,程序返回的调用,从内核态返回用户态时触发 OsRestorSignalContext(regs);//恢复信号上下文,执行完函数后,切到了用户栈 return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://@note_thinking 如何是两个参数的系统调用,这里传的确是三个参数,任务栈中会出现怎样的情况呢? case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//保存用户栈现场 /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with theSYS_context_switch system call. */ return regs;//返回寄存器的值 }
解读
- 参数是
regs
对应的就是R0~Rn - R7保存的是系统调用号,R0~R3保存的是
SysMqOpen
的四个参数 g_syscallHandle[cmd]
就能查询到SYSCALL_HAND_DEF(__NR_mq_open, SysMqOpen, mqd_t, ARG_NUM_4)
注册时对应的SysMqOpen
函数*(SyscallFun5)handle
此时就是SysMqOpen
- 注意看 SysMqOpen 的参数是最开始的
main
函数中的mqdes = mq_open(mqname, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr);
由此完成了真正系统调用的过程
7. SysMqOpen
mqd_t SysMqOpen(const char *mqName, int openFlag, mode_t mode, struct mq_attr *attr) { mqd_t ret; int retValue; char kMqName[PATH_MAX + 1] = { 0 }; retValue = LOS_StrncpyFromUser(kMqName, mqName, PATH_MAX); if (retValue < 0) { return retValue; } ret = mq_open(kMqName, openFlag, mode, attr);//一个消息队列可以有多个进程向它读写消息 if (ret == -1) { return (mqd_t)-get_errno(); } return ret; }
解读
- 此处的
mq_open
和main函数的mq_open
其实是两个函数体实现.一个是给应用层的调用,一个是内核层使用,只是名字一样而已. SysMqOpen
是返回到OsArmA32SyscallHandle
regs[REG_R0] = ret;
OsArmA32SyscallHandle
再返回到_osExceptSwiHdl
_osExceptSwiHdl
后面的代码是用于恢复用户模式现场和SPSR
,PC
等寄存器.
以上为鸿蒙系统调用的整个过程.
关于寄存器(R0~R15)在每种模式下的使用方式,后续将由其他篇详细说明,敬请关注.
喜欢就大方 点赞+关注+收藏 吧
各大站点搜 "鸿蒙内核源码分析" ,快速找到组织.
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深入探究JDK中Timer的使用方式与源码解析
导言 定时器Timer的使用 构造方法 实例方法 使用方式 1. 执行时间晚于当前时间 2. 执行时间早于当前时间 3. 向Timer中添加多个任务 4. 周期性执行任务 5. 停止任务 源码解析 TimerTask TimerTask对象的成员 TimerTask对象的构造方法 TimerTask对象的成员方法 TimerQueue 二叉堆(Binary heap) TimerQueue的完整代码 TimerThread Timer Timer对象的成员 Timer对象的构造方法 成员函数 总结 导言 在项目开发过程中,经常会遇到需要使用定时执行或延时执行任务的场景。比如我们在活动结束后自动汇总生成效果数据、导出Excel表并将文件通过邮件推送到用户手上,再比如微信运动每天都会在十点后向你发送个位数的步数(在?把摄像头从我家拆掉!)。 本文将会介绍java.util.Timer的使用,并从源码层面对它进行解析。 定时器Timer的使用 java.util.Timer是JDK提供的非常使用的工具类,用于计划在特定时间后执行的任务,可以只执行一次或定期重复执行。在JDK内部很多组件都是使...
- 下一篇
手把手教你如何制作可视化大屏!
>>《ECharts数据可视化:入门、实战与进阶》是一本由 ECharts 官方推荐,系统全面、由浅入深、注重实操,带领读者快速从新人到高手的书籍。欢迎参与文末赠书活动 。 在我们生活和工作中,经常会看到可视化大屏,主要用于展示一些汇总信息,那如何制作一个酷炫的可视化大屏呢? 今天带大家看个github上的案例。 项目地址:https://github.com/yyhsong/iDataV/tree/master/case09 效果如下: 可以看到,这是一个名为“上市公司全景概览”的可视化大屏,展示内容包含了上市公司地域分布、行业分布、股票情况及预测、市值排行、上市公司数等。展示形式包含环形图、双轴图(柱状图+折线图)、地图、条形图、数值。 接下来,我们从代码结构拆解一下这个案例。 首先,通过下图可以看出该项目由5部分组成。 分别是css、data、img、js和html,其中css是设置一些样式、data是数据来源,打开后可以看到是json格式数据,如下图所示。 Img中是一些图,我们可以看到,其中有背景图,也有一些展示的符号图,如下图所示。 大屏的header部分: 大...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,8上快速安装Gitea,搭建Git服务器