十篇文章理解OS内核(1)用户与内核的桥梁---系统调用
1.什么是系统调用
在 linux 中,软硬件是有明显的分层的,出于安全或者是资源统筹考虑,硬件资源由内核进行统一管理,内核拥有绝对的权限,而用户空间无法直接访问硬件.在实际的应用中,用户进程总是无法避免需要操作到硬件,这个硬件可能是磁盘文件,USB接口等,这时候就需要向内核递交申请,让内核帮忙做硬件相关的事情,这个过程就由系统调用完成.
无论从硬件还是从软件角度来说,用户空间与内核空间有一道无法轻易逾越的屏障,如果是简单地一分为二,事情并不会有多复杂,不幸的是,这两者不能简单地完全隔断,用户空间的大部分操作都需要通过内核来完成,就连简单的申请内存操作,用户空间都无法独立自主地做到,因为这涉及到物理内存的分配,而物理内存也是硬件的一种,所以在这道屏障上需要开一扇门,来进行内核与用户之间的交互,这道门也就是系统调用.
2.系统调用的作用和重要性
系统调用是用户空间程序与内核空间之间的桥梁。用户程序通过系统调用请求内核提供的服务,如文件操作、进程控制、网络通信等。系统调用的存在有以下几个重要原因:
- 安全性:保护系统资源,防止用户程序滥用系统资源。
- 抽象性:为用户程序提供统一的接口,隐藏底层实现细节。
- 可扩展性:内核可以根据需要添加新的服务,而不影响用户程序。
3.系统调用的实现机制
系统调用的实现涉及到用户态和内核态之间的切换,以及内核内部的处理流程。下面是一个简化的系统调用流程图:
-
用户程序发起系统调用:用户程序通过系统调用接口(如
syscall
函数)发起系统调用请求。 -
切换到内核态:系统调用接口触发一个中断或特殊指令,使得CPU从用户态切换到内核态。
-
查找系统调用表:内核通过系统调用表(
sys_call_table
)查找对应的系统调用处理函数。 -
执行处理函数:内核调用相应的处理函数来执行系统调用的具体逻辑。
-
返回用户态:系统调用完成后,内核将结果返回给用户程序,并通过中断返回指令切换回用户态。
4.系统调用指令都干了些什么(以syscall指令为例)
程序想从用户态进入内核态的关键就是系统调用指令,这个指令是内核和用户的桥梁。
syscall
指令是x86-64架构中用于进行系统调用的一种机制。和sysret指令成对出现。当用户态程序需要执行系统调用时,它通过syscall
指令触发一个平滑的从用户态到内核态的过渡。以下是syscall
指令执行时所做的事情的详细步骤:
4.1. 保存用户态寄存器
在执行syscall
指令之前,用户程序会将需要传递给内核的参数放入特定的CPU寄存器中。在x86-64架构中,通常是以下寄存器:
rdi
,rsi
,rdx
,r10
,r8
,r9
:用于传递前六个参数。rax
:用于存放系统调用的编号。
4.2. 执行syscall
指令
用户程序执行syscall
指令,这会导致CPU执行以下操作:
- 触发中断:
syscall
指令在x86-64架构中相当于执行了一个中断,通常是中断向量号0x80(32位系统)或0x0C0(64位系统)。
4.3. 从用户态切换到内核态
中断发生后,CPU会自动从用户态切换到内核态。这个过程中,CPU会做以下事情:
- 更改栈:从用户栈切换到内核栈。
- 更改指令指针:将指令指针指向内核态的中断处理程序。
- 保存状态:保存用户态的CPU状态,包括寄存器内容等,以便于之后可以恢复。
4.4. 传递系统调用信息
内核态的中断处理程序会检查由用户态传递过来的系统调用号和参数,并根据这些信息确定要执行的系统调用。
4.5. 查找系统调用处理函数
内核使用系统调用号查找sys_call_table
,找到对应的系统调用处理函数。
4.6. 执行系统调用处理函数
内核调用相应的处理函数执行系统调用。这个处理函数会实际执行用户请求的操作,如读写文件、创建进程等。
4.7. 处理系统调用结果
系统调用完成后,处理函数会将结果返回。成功时,返回值通常是一个非负整数;出错时,返回一个负的错误码。
4.8. 从内核态返回用户态
系统调用完成后,内核需要将控制权交还给用户程序。这涉及到以下步骤:
- 恢复用户态寄存器:将用户态的寄存器状态恢复。
- 返回用户栈:从内核栈切换回用户栈。
- 使用
sysret
指令:在x86-64架构中,sysret
指令会从内核态返回到用户态。
4.9. 继续用户程序的执行
用户程序在syscall
指令之后的下一条指令处继续执行,此时系统调用的结果已经被返回,用户程序可以根据这个结果进行后续操作。
syscall
指令的设计使得系统调用的发起和返回都非常简单和高效,同时确保了用户态和内核态之间的隔离,提高了系统的安全性和稳定性。
5.系统调用表的结构
系统调用表是一个函数指针数组,每个元素对应一个系统调用的处理函数。下面是一个简化的系统调用表的结构图:
sys_ni_syscall
:默认的处理函数,当系统调用编号无效时,返回错误。sys_read
、sys_write
、sys_open
等:具体的系统调用处理函数。
6.添加新的系统调用
要向内核添加一个新的系统调用,需要以下步骤:
- 修改系统调用表:在内核源码的
syscall_64.tbl
文件中添加新的系统调用编号和对应的处理函数。
common mycall __x64_sys_mycall
- 实现处理函数:编写一个新的C函数来实现你的系统调用逻辑,并使用
asmlinkage
关键字声明。
asmlinkage long sys_mycall(struct pt_regs *regs) {
// 实现系统调用逻辑
return 0;
}
-
修改内核配置:如果系统调用函数分布在不同的源文件中,需要在相应的
makefile
中添加源文件的引用。 -
编译内核:修改完成后,重新编译内核,这样你的新系统调用就可以被使用了。
系统调用的性能考虑
系统调用的实现方式对性能有重要影响。早期的系统调用通过软件中断(如int $0x80
)实现,这种方式简单但性能较差。现代的系统调用通过专门的CPU指令(如syscall
)实现,可以更快地在用户态和内核态之间切换。
总结
系统调用是我们在传统的用户程序思维中,对OS内核的初步窥探,理解系统调用,是我们理解内核最初也是最重要的一步。
#操作系统##大厂##OS##嵌入式##开发#笔者在求职嵌入式,C/C++,后台开发等岗位的技术面中,基本所有面试官对笔者具有OS内核相关开发经历十分感兴趣,很多面试官也会建议我再继续深入的了解OS内核。如华为,vivo,小米,海康的底软岗位的面试官,会一直深挖我的OS大赛项目。 对于计算机专业,OS一直是重中之重,但是现代内核很多机制的实现细节,学校并没有讲明,所以我会输出一些文章来详细讲讲OS的实现细节以及面试中需要着重铭记的要点。