『阿男的Linux内核世界』*14 从User Space到Kernel Space(二)*
我们知道了Intel架构的CPU的指令权限是按照Ring的级别来进行区分的:Kernel Space会使用Ring 0
获得CPU的全部使用和管理权限,User Space使用Ring 3
,获得计算机有限的执行权限。当你在User Space强行使用高权限的CPU指令时,CPU会直接报错的,而Kernel也会把这个错误返回给你。
因此,一些不需要更高权限的指令,比如ADD
,SUB
这些基本的运算指令,是直接在User Space里面执行的。此外,还有一些User Space的内存的数据移动和Copy是直接在User Space执行的,对应的指令比如MOV
,LEA
等等。
当然,如果你超出内存边界范围地去移动一些数据,CPU也会给你报错的。因为我们的Process是运行在Kernel提供给我们的虚拟内存空间里。这种模式对于CPU叫做Protected Mode
,也就是保护模式。
所谓保护模式,就是CPU会保护每一个Process在它们各自的虚拟内存地址空间里面执行而不会威胁到Kernel和其它Process的数据安全。这当然也是通过只有Kernel能访问的一些高权限的寄存器来辅助Kernel的代码来完成的。关于具体实现,阿男后续再给大家细讲。
接下来我们可以想一下什么时候Process需要从User Space跳到Kernel Space去执行,也就是说,Process是怎么样从User Space跳到Kernel Space的。
这个问题的答案是:当系统接收到「中断请求」的时候,就会从User Space转到Kernel Space。什么是「中断请求」?「中断请求」大概分为三种情况:
- CPU异常
- 硬件中断
- 软件中断
CPU异常就是指CPU在工作过程中发生的各种错误,比如运算的时候用户写的代码除以0了,或者是Ring 3
模式下要执行Ring 0
才允许的指令,等等。
硬件中断,就是指计算机的硬件设备给CPU发信号,告诉CPU,我这个硬件设备有信息给你处理。比如我们按键盘,就是一个硬件中断:每次我们按键,键盘都得告诉CPU用户按的是哪个键,这样CPU才知道有用户通过键盘输入的数据要处理。
软件中断就是使用CPU提供的INT
指令再加上中断编号,通过指令来生成一个中断。
Intel CPU的架构为以上三种中断支持从0
号到255
号中断,也就是说,一共256个编号可以用来定义中断,但这里面有一些预留的中断编号是CPU的架构预先约定好的。
比如,前32个编号是固定定义好的,这前32个中断大多和CPU自身的状态相关,我们可以在traps.h
里面找到相关的定义^1:
126 /* Interrupts/Exceptions */
127 enum {
128 X86_TRAP_DE = 0, /* 0, Divide-by-zero */
129 X86_TRAP_DB, /* 1, Debug */
130 X86_TRAP_NMI, /* 2, Non-maskable Interrupt */
131 X86_TRAP_BP, /* 3, Breakpoint */
132 X86_TRAP_OF, /* 4, Overflow */
133 X86_TRAP_BR, /* 5, Bound Range Exceeded */
134 X86_TRAP_UD, /* 6, Invalid Opcode */
135 X86_TRAP_NM, /* 7, Device Not Available */
136 X86_TRAP_DF, /* 8, Double Fault */
137 X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */
138 X86_TRAP_TS, /* 10, Invalid TSS */
139 X86_TRAP_NP, /* 11, Segment Not Present */
140 X86_TRAP_SS, /* 12, Stack Segment Fault */
141 X86_TRAP_GP, /* 13, General Protection Fault */
142 X86_TRAP_PF, /* 14, Page Fault */
143 X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */
144 X86_TRAP_MF, /* 16, x87 Floating-Point Exception */
145 X86_TRAP_AC, /* 17, Alignment Check */
146 X86_TRAP_MC, /* 18, Machine Check */
147 X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */
148 X86_TRAP_IRET = 32, /* 32, IRET Exception */
149 };
此外,剩下的中断编号的定义,我们还可以在irq_vectors.h
^2里面看到,这里面也包含了好多硬件中断的约定编号。在这里我们重点要看的就是0x80
号中断,其中0x80
是16进制,对应十进制的128
。下面是0x80
号中断在irq_vectors.h
中的定义:
49 #define IA32_SYSCALL_VECTOR 0x80
这个0x80
,也就是十进制的128
号中断,就是对应所有System calls的入口。所谓System calls,就是Kernel Space给User Space的API接口,用于User Space请求Kernel Space的一些功能。软中断最常用的也就是0x80
号中断,所以我们可以在汇编代码中看到大量INT 0x80
指令的调用,这也是一种CPU和操作系统中的约定。
当User Space想使用Kernel Space提供的这些System calls的时候,就把需要的System call编号和要传递的参数放到寄存器里,然后执行INT 0x80
指令,就会跳进System call的处理函数。
我们可以大概想一下这个处理函数要怎么实现,首先我们要有一张表来保存各个System calls对应的实现函数,然后我们需要有一个入口函数,查表去执行请求的具体函数。关于这个表和这个入口函数的具体函数,阿男就不具体说了,可以看这篇文档^3,如果你不是很关心具体的实现,只要理解这里面的思路就好 。我们这里看一下Linux默认的一些System calls的列表:
#define __NR_open 1024
__SYSCALL(__NR_open, sys_open)
#define __NR_link 1025
__SYSCALL(__NR_link, sys_link)
#define __NR_unlink 1026
__SYSCALL(__NR_unlink, sys_unlink)
#define __NR_mknod 1027
__SYSCALL(__NR_mknod, sys_mknod)
#define __NR_chmod 1028
__SYSCALL(__NR_chmod, sys_chmod)
#define __NR_chown 1029
__SYSCALL(__NR_chown, sys_chown)
#define __NR_mkdir 1030
__SYSCALL(__NR_mkdir, sys_mkdir)
#define __NR_rmdir 1031
__SYSCALL(__NR_rmdir, sys_rmdir)
#define __NR_lchown 1032
__SYSCALL(__NR_lchown, sys_lchown)
#define __NR_access 1033
__SYSCALL(__NR_access, sys_access)
#define __NR_rename 1034
__SYSCALL(__NR_rename, sys_rename)
#define __NR_readlink 1035
__SYSCALL(__NR_readlink, sys_readlink)
#define __NR_symlink 1036
__SYSCALL(__NR_symlink, sys_symlink)
#define __NR_utimes 1037
__SYSCALL(__NR_utimes, sys_utimes)
#define __NR3264_stat 1038
__SC_3264(__NR3264_stat, sys_stat64, sys_newstat)
#define __NR3264_lstat 1039
__SC_3264(__NR3264_lstat, sys_lstat64, sys_newlstat)
最新的Linux内核里面有300多个system calls,涵盖操作系统所提供的方方面面的功能,比如上面阿男列出来的一小部分。上面这些system calls定义在include/uapi/asm-generic/unistd.h
里面^4。我们可以看看这些System calls的功能分类,有控制Process生命周期的,比如exit
,fork
等等。还有控制IO设备输入输出的,比如read
,write
等等。还有控制各种IO设备资源的打开和关闭的,比如open
,close
。