linux系统调用原理及实现

linux系统调用

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

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

用户态调用接口

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

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

int syscall(int number, ...);

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

需要注意的是,这里的返回值和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,这个数据结构在<arch/x86/entry/syscall_64.c>中定义如下:

/* 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 <asm/syscalls_64.h>
#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 <asm/syscalls_64.h>
};

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

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

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

需要注意的是<asm/syscalls_64.h>和提供系统调用号宏定义的头文件<asm/unistd_64.h>等文件在内核源码树中是不存在的,会在内核编译的预编译阶段自动生成。内核源码中真正定义系统调用号和处理函数的文件,是<arch/x86/entry/syscalls/syscall_64.tbl>,该文件的内容格式如下:

#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# 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限定去掉,然后在运行中切换调用处理函数。

  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值