学习并整理了下open等系统调用,从用户态如何调用到内核态的全过程。
1.Linux内核目录总览
2.Linux文件系统与设备驱动关系
这是在Linux设备驱动开发详解里找的两张图,内容很形象。
当用户程序通过系统调用陷入内核态时,会先经过VFS,也就是虚拟文件系统,使用不同的file_operations,在这里会根据操作的文件类型,来进行不同操作。
3.系统调用完成内核调用全过程详解
3.1 用户态
- 确认好自己使用的glibc版本,下载对应版本的源码
这里我使用的是GLIBC_2.18,所以下载的是glibc2.18版本源码,解压到目录中查看
arm-linux-gnueabihf-strings ./arm-linux-gnueabihf/libc/lib/libc.so.6 | grep GLIBC_ GLIBC_2.4 GLIBC_2.5 GLIBC_2.6 GLIBC_2.7 GLIBC_2.8 GLIBC_2.9 GLIBC_2.10 GLIBC_2.11 GLIBC_2.12 GLIBC_2.13 GLIBC_2.14 GLIBC_2.15 GLIBC_2.16 GLIBC_2.17 GLIBC_2.18 GLIBC_PRIVATE
- 跟踪open函数调用
1)首先是open函数其实是一个宏定义,实现为open_not_cancel_2函数,再转为INLINE_SYSCALL宏定义
glibc-2.18/intl/loadmsgcat.c
#ifdef _LIBC /* Rename the non ISO C functions. This is required by the standard because some ISO C functions will require linking with this object file and the name space must not be polluted. */ # define open(name, flags) open_not_cancel_2 (name, flags) # define close(fd) close_not_cancel_no_status (fd) # define read(fd, buf, n) read_not_cancel (fd, buf, n) # define mmap(addr, len, prot, flags, fd, offset) \ __mmap (addr, len, prot, flags, fd, offset) # define munmap(addr, len) __munmap (addr, len) #endif
glibc-2.18/sysdeps/unix/sysv/linux/not-cancel.h
#define open_not_cancel_2(name, flags) \ INLINE_SYSCALL (open, 2, (const char *) (name), (flags))
2)因为是arm架构,所以看arm架构这边的INLINE_SYSCALL函数,其实调用的是INTERNAL_SYSCALL,这里又调用到了INTERNAL_SYSCALL_RAW,再到LOAD_ARGS_2宏定义,这里比较重要的是INTERNAL_SYSCALL_RAW宏定义,大概的解读在下面
glibc-2.18/ports/sysdeps/unix/sysv/linux/arm/sysdep.h
#define INLINE_SYSCALL(name, nr, args...) \ ({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \ if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \ { \ __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \ _sys_result = (unsigned int) -1; \ } \ (int) _sys_result; })
glibc-2.18/ports/sysdeps/unix/sysv/linux/arm/sysdep.h
#define ASM_ARGS_0 #define LOAD_ARGS_1(a1) \ int _a1tmp = (int) (a1); \ LOAD_ARGS_0 () \ _a1 = _a1tmp; #define ASM_ARGS_1 ASM_ARGS_0, "r" (_a1) #define LOAD_ARGS_2(a1, a2) \ int _a2tmp = (int) (a2); \ LOAD_ARGS_1 (a1) \ register int _a2 asm ("a2") = _a2tmp; #define ASM_ARGS_2 ASM_ARGS_1, "r" (_a2) #define SYS_ify(syscall_name) (__NR_##syscall_name) #define INTERNAL_SYSCALL(name, err, nr, args...) \ INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args) # define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \ ({ \ register int _a1 asm ("r0"), _nr asm ("r7"); \ LOAD_ARGS_##nr (args) \ _nr = name; \ asm volatile ("swi 0x0 @ syscall " #name \ : "=r" (_a1) \ : "r" (_nr) ASM_ARGS_##nr \ : "memory"); \ _a1; }) #endif
3)对于SYS_ify宏定义,是将传入的syscall_name转为__NR_##syscall_name宏定义,也就是说syscall_name是open时,这个宏代表的便是__NR_open,而_NR_open的值为5。
比较关键的是执行INTERNAL_SYSCALL_RAW时,首先是将变量映射到寄存器中,并且执行swi软中断指令,触发syscall系统调用,并且将__NR_open宏进行传递,告知我们调用的是哪一个系统调用。
#define SYS_ify(syscall_name) (__NR_##syscall_name) # define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \ ({ \ register int _a1 asm ("r0"), _nr asm ("r7"); \ //映射int类型a1变量到r0寄存器,映射__nr变量到r7寄存器 LOAD_ARGS_##nr (args) \ //LOAD_ARGS_##nr 展开便是LOAD_ARGS_2(const char *) (name), (flags) _nr = name; \ //nr 变量 = name = __NR_open = #define __NR_open 45 asm volatile ("swi 0x0 @ syscall " #name \ //asm volatile,插入汇编代码执行,swi 0x0是汇编的软中断指令,用来触发syscall系统调用 : "=r" (_a1) \ //=r是输出操作,使用通用寄存器存储结果,并将结果输出到_a1变量 : "r" (_nr) ASM_ARGS_##nr \ //r是输入操作,将__nr,_a1 ,_a2变量作为参数,这里也会使用通用寄存器来传递值 : "memory"); \ //表示内存约束,在执行这段汇编代码之前,需要刷新所有内存数据 _a1; }) //_a1作为整个宏的返回值 #endif /* #define ASM_ARGS_1 ASM_ARGS_0, "r" (_a1) #define LOAD_ARGS_2(a1, a2) \ int _a2tmp = (int) (a2); \ LOAD_ARGS_1 (a1) \ register int _a2 asm ("a2") = _a2tmp; #define ASM_ARGS_2 ASM_ARGS_1, "r" (_a2) /*
arch/arm64/include/asm/unistd32.h
#define __NR_open 5 __SYSCALL(__NR_open, compat_sys_open)
4)当发生系统调用(syscall)时,触发了软中断,处理器切换到内核态,在arm系列中,此时执行内核中的vector_swi函数,此时获取系统调用号scno,并根据系统调用号从sys_call_table中找到对应的系统调用函数并执行。
其中sys_call_table的内容,便是根据calls.S其中的内容,根据call的定义可以知道,这里其实就是在做一个数组的填充,当我们open一个函数,scno是5,对应也就调用到sys_open函数
arch/arm/kernel/entry-common.S
ENTRY(vector_swi) #ifdef CONFIG_CPU_V7M v7m_exception_entry #else sub sp, sp, #S_FRAME_SIZE stmia sp, {r0 - r12} @ Calling r0 - r12 ARM( add r8, sp, #S_PC ) ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr THUMB( mov r8, sp ) THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr mrs r8, spsr @ called from non-FIQ mode, so ok. str lr, [sp, #S_PC] @ Save calling PC str r8, [sp, #S_PSR] @ Save CPSR str r0, [sp, #S_OLD_R0] @ Save OLD_R0 #endif zero_fp alignment_trap r10, ip, __cr_alignment enable_irq ct_user_exit get_thread_info tsk /* * Get the system call number. */ #if defined(CONFIG_OABI_COMPAT) /* * If we have CONFIG_OABI_COMPAT then we need to look at the swi * value to determine if it is an EABI or an old ABI call. */ #ifdef CONFIG_ARM_THUMB tst r8, #PSR_T_BIT movne r10, #0 @ no thumb OABI emulation USER( ldreq r10, [lr, #-4] ) @ get SWI instruction #else USER( ldr r10, [lr, #-4] ) @ get SWI instruction #endif ARM_BE8(rev r10, r10) @ little endian instruction #elif defined(CONFIG_AEABI) /* * Pure EABI user space always put syscall number into scno (r7). */ #elif defined(CONFIG_ARM_THUMB) /* Legacy ABI only, possibly thumb mode. */ tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in USER( ldreq scno, [lr, #-4] ) #else /* Legacy ABI only. */ USER( ldr scno, [lr, #-4] ) @ get SWI instruction #endif adr tbl, sys_call_table @ load syscall table pointer
ENTRY(sys_call_table) #include "calls.S" //calls.S其中一部分内容 /* 0 */ CALL(sys_restart_syscall) CALL(sys_exit) CALL(sys_fork) CALL(sys_read) CALL(sys_write) /* 5 */ CALL(sys_open) CALL(sys_close) CALL(sys_ni_syscall) /* was sys_waitpid */ CALL(sys_creat) CALL(sys_link)
至此,便成功由用户态open系统调用,成功调用到内核中的sys_open函数,这便是一个较为详细的全过程。
之后再继续跟踪下从sys_open到各个文件系统fops操作符的过程(主要整理上面这些累的吐血了…摸摸鱼…)
4.总结
open系统调用从内核态切换到内核态全过程:
用户调用glibc接口Open函数
->调用到open_not_cancel_2宏定义->INLINE_SYSCALL宏定义->INTERNAL_SYSCALL_RAW宏定义
->系统调用的scno映射到寄存器值,并执行swi软中断指令
->此时进入内核态,触发syscall系统调用,执行内核的vevtor_swi函数
->获取scno,并根据call.s组成的sys_call_table中找到对应的系统调用函数
->执行函数,调用retq指令返回用户态