linux系统调用原理及实现

linux系统调用

系统调用是linux内核为用户态程序提供的主要功能接口。通过系统调用,用户态进程能够临时切换到内核态,使用内核态才能访问的硬件和资源完成特定功能。系统调用由linux内核和内核模块实现,内核在处理系统调用时还会检查系统调用请求和参数是否正确,保证对特权资源和硬件访问的正确性。通过这种方式,linux在提供内核和硬件资源访问接口的同时,保证了内核和硬件资源的使用正确性和安全性。

本文主要对linux下系统调用的原理和实现进行分析。本文的分析基于x86架构,涉及到的linux内核代码版本为4.17.6。

用户态调用接口

用户态进程主要通过如下方式,直接使用系统调用:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include 
#include    /* For SYS_xxx definitions */

int syscall(int number, ...);

syscall接口由glibc提供和实现,第一个参数number表示需要调用的系统调用编号,后续的可变参数根据系统调用类型确定。内核具体支持的系统调用号可在中查看。函数调用失败会返回-1,具体错误原因保存在errno中,errno的含义可参考

需要注意的是,这里的返回值和errno是glibc封装提供的,内核的系统调用响应函数本身不提供errno,返回值也不同。

实现原理

一次系统调用的完整执行过程如下:

  1. 通过特定指令发出系统调用(int $80、sysenter、syscall)

  2. CPU从用户态切换到内核态,进行一些寄存器和环境设置

  3. 调用system_call内核函数,通过系统调用号获取对应的服务例程

  4. 调用系统调用处理例程

  5. 使用特定指令从系统调用返回用户态(iret、sysexit、sysret)

系统调用指令

向内核发起系统调用需要使用特定的指令。在Linux中,传统的方法是使用汇编指令int发起中断,使用0x80(128)号中断使CPU进入内核态,之后调用对应的中断响应函数system_call来执行系统调用例程。

由于通过中断方式发起系统调用的性能较差,较新的CPU和内核都支持使用sysenter和syscall这两条专用指令来发起系统调用。其中sysenter在32位系统中使用,对应的退出指令为sysexit;syscall在64位系统中使用,对应的退出指令为sysret。

以sysenter为例,使用该指令时,首先调用__kernel_vsyscall()函数保存用户态堆栈;之后执行sysenter指令切换到内核态;最后开始执行sysenter_entry()函数设置内核态堆栈,并根据系统调用号调用处理例程,之后的逻辑和system_call相似。

内核实现逻辑

内核调用系统调用处理例程的核心数据结构是sys_call_table,这个数据结构在中定义如下:

/* this is a lie, but it does not hurt as sys_ni_syscall just returns -EINVAL */
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include 
#undef __SYSCALL_64

#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	 * Smells like a compiler bug -- it doesn't work
	 * when the & below is removed.
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include 
};

sys_call_table是一个函数指针数组,其中保存了所有系统调用处理函数的指针。system_call等函数以系统调用号作为下标,从sys_call_table中查找对应的系统调用函数执行。

sys_call_table的初始化过程中,第一步是将所有指针数组元素赋值为sys_ni_syscall。这是为了避免有部分系统调用号没有被使用,没有定义对应的处理函数。sys_ni_syscall在中定义,直接返回-ENOSYS,表示系统调用不存在。

sys_call_table的具体内容在中提供,内容类似于:__SYSCALL_64(19, sys_readv, sys_readv)。从之前对__SYSCALL_64宏的两处定义可见,syscall_64.c先将__SYSCALL_64宏展开为函数声明extern asmlinkage long sym(const struct pt_regs *),再将其展开为数组元素初始化语句[nr] = sym。

需要注意的是和提供系统调用号宏定义的头文件等文件在内核源码树中是不存在的,会在内核编译的预编译阶段自动生成。内核源码中真正定义系统调用号和处理函数的文件,是,该文件的内容格式如下:

#
# 64-bit system call numbers and entry vectors
#
# The format is:
#    
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0	common	read			__x64_sys_read
1	common	write			__x64_sys_write
2	common	open			__x64_sys_open
3	common	close			__x64_sys_close
4	common	stat			__x64_sys_newstat
5	common	fstat			__x64_sys_newfstat
6	common	lstat			__x64_sys_newlstat
7	common	poll			__x64_sys_poll
8	common	lseek			__x64_sys_lseek
9	common	mmap			__x64_sys_mmap

内核预编译系统根据这个文件中提供的系统调用号、系统调用名称和对应的处理函数名称来生成对应的头文件。

添加新的系统调用

根据上述分析,如果需要添加一个新的系统调用号和处理函数,需要完成如下修改:

  1. 在syscall_64.tbl中添加新的系统调用号、名称和处理函数名称。例如“666  common  mycall   __x64_sys_mycall”

  2. 提供sys_mycall函数实现。函数应定义为asmlinkage long sys_mycall(...)

  3. 如果sys_mycall函数实现在独立的.c文件中,需要将其加入lib/路径下的makefile中,在obj-y中添加.c文件路径

之后重新编译内核即可提供自定义的系统调用功能。

需要注意的是,sys_call_table数据结构在源码中是一个const变量,因此系统调用函数指针初始化完成后是不能修改的。如果需要在运行中动态修改或添加系统调用处理函数(例如通过可加载内核模块来提供处理函数),可以将const限定去掉,然后在运行中切换调用处理函数。

你可能感兴趣的:(linux)