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

学习并整理了下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指令返回用户态

  • 29
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值