学习并整理了下open等系统调用,从用户态如何调用到内核态的全过程。
这是在Linux设备驱动开发详解里找的两张图,内容很形象。
当用户程序通过系统调用陷入内核态时,会先经过VFS,也就是虚拟文件系统,使用不同的file_operations,在这里会根据操作的文件类型,来进行不同操作。
这里我使用的是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
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操作符的过程(主要整理上面这些累的吐血了…摸摸鱼…)
open系统调用从内核态切换到内核态全过程:
用户调用glibc接口Open函数
->调用到open_not_cancel_2宏定义->INLINE_SYSCALL宏定义->INTERNAL_SYSCALL_RAW宏定义
->系统调用的scno映射到寄存器值,并执行swi软中断指令
->此时进入内核态,触发syscall系统调用,执行内核的vevtor_swi函数
->获取scno,并根据call.s组成的sys_call_table中找到对应的系统调用函数
->执行函数,调用retq指令返回用户态