Linux内核(4)——Linux设备文件open函数从应用到内核全过程解析

avatar
作者
猴君
阅读量:0

学习并整理了下open等系统调用,从用户态如何调用到内核态的全过程。

1.Linux内核目录总览

image.png
image.png

2.Linux文件系统与设备驱动关系

这是在Linux设备驱动开发详解里找的两张图,内容很形象。
当用户程序通过系统调用陷入内核态时,会先经过VFS,也就是虚拟文件系统,使用不同的file_operations,在这里会根据操作的文件类型,来进行不同操作。
image.png
image.png

3.系统调用完成内核调用全过程详解

3.1 用户态

  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 
  1. 跟踪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指令返回用户态

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!