1. 讲系统调用前需要清楚的几个基本概念
1.1 内核态与用户态
- 内核态:当CPU执行内核的代码(CPU堆栈指针指向内核堆栈)时,我们就称此时处于内核态,内核态的代码可以使用特权指令,这些指令可以控制计算机,直接操作计算的硬件,如in,out等;
- 用户态:CPU执行的是用户写的代码(CPU堆栈指针指向内核堆栈),此时就称处于用户态,用户态不可以使用特权指令,都是一些基础的指令,如mov,add,sub。
1.2 中断
- 中断分为硬件中断和软中断,硬件中断指的是外设的中断,由中断控制器提供的
- 软中断,这里说的是软件中断,仅通过中断指令触发的中断。
硬件中断和软中断之间的差别
区分中断、异常、系统调用三个概念
1.3 linux下int 0x80号中断
linux下所有的系统调用都和0x80号中断绑定,当用户或者库函数调用一个系统提供的API,就会产生一个0x80的中断,CPU将陷入内核,去执行内核代码,完成用户或者程序的请求。
1.4 Linux下的特权级别
X86体系结构支持的所有特权级别:
Linux内核中实际所采用的特权级别:
- R0:最高级别,也就是操作系统级别的权限,可以访问所有的资源。即内核态(管理态)
- R3:最低级别,也就是用户权限,无法直接访问硬件资源,必须通过操作系统提供的接口访问。即用户态。
Linux下用户和内核状态如何转换:
- 用户态到内核态:我们称之为陷入内核,主要的途径有中断、异常、陷入机制;
- 内核态到用户态:我们称之为陷出,修改程序状态字PSW即可。
1.5 中断向量表:
存放中断处理程序的入口地址
2. 系统调用:编程者向操作系统提出服务请求的接口
2.1 作用和意义:使用户态陷入内核态,才能得到资源
- 是用户与内核之间沟通的桥梁;
- 我们知道我们计算机几乎所有的硬件等资源都由我们的操作系统管理,不论是我们还是我们自己编写的软件再或者用户的应用软件对计算机硬件或者其他资源进行操作时都需要操作系统的“同意”,这些资源由操作系统给我们统一保管和分配。
2.2 一个系统调用基本过程
2.3 系统调用write()中缓冲区
一般默认将我们目标内容打印到屏幕上,实际上底层调用的是我们的write(),将目标的字符传送到我们的标准输出
实际上,上述的write()并不是每次执行都会进行中断来满足用户,无论是中断还是系统调用,都会产生巨大的开销,无论是时间上的还是空间上的。
write()函数有自己缓冲区,一般来说,要等到缓冲区满或者用户不在写入了,write才切换到内核态执行,这样做大大加快系统的运行效率。
3. 内核态和用户态切换
3.1 堆栈以及堆栈寄存器简单介绍
3.1.1 CPU堆栈寄存器,作用如下:
- 当cpu的堆栈寄存器指向的是用户的地址空间,说明系统现在处于用户态,正在执行用户空间的代码;
- 当cpu的堆栈寄存器指向的是内核的地址空间,说明系统正在处于内核态,正在执行内核空间的代码。
3.1.2 用户堆栈以及内核堆栈
- 内核栈:是内核固有的一块区域,用来保护中断现场,内核中 函数 之间相互调用的参数,返回值等。
- 用户栈:专门用于用户执行各种各样的函数的内存,随函数调用开始结束共存亡
3.2 用户态——>内核态:
cpu保存用户当前堆栈信息保存到内核的栈中(恢复时用到),然后将cpu指向内核堆栈,去执行内核代码。
3.3 内核态——>用户态:
再切换到内核堆栈前,将用户堆栈信息压入到内核栈中,内核函数执行完回退栈帧,会将用户的堆栈信息POP出栈,然后cpu堆栈寄存器就知道怎么回去了,返回的用户程序中断的地方继续执行。
4. 补充一个write()调用的大致过程
- 1.我在我的 应用程序 中执行调用write库函数:
int main()
{
...
write(...); //应用程序中 调用 库函数 write()
...
return 0;
}
- 2.库函数做的事情:
将参数收集起来 + 启动0x80号中断陷入内核实际是调用system_call()
系统调用号就是system_call()进行推送的
- 3.陷入内核中做的事情:查系统调用表 + 调响应的系统调用 这里是sys_write() 然后给用户返回
xxxxxxxxxxxx sys_writen()
{
...
return something;
}
整个过程:应用程序->库函数->内核
再来看看最终的堆栈是什么样的:
其中,系统调用号之前的东西 由 硬件机制 push入栈,系统调用号由system_call() push入栈
5. 自己设计一个系统调用过程如何设计
-
利用硬件的支持----中断/异常机制:实现支持系统调用服务
-
选择一条访管指令(陷入指令)-----用来陷入内核,引发异常完成用户和内核态的切换
-
为每一个系统调用 进行编号(因为所有系统调用通过同一条陷入指令进行,所以需要编号方便来查找程序);
参数:编译器选择如何将参数传递(寄存器或者堆栈?都可以) -
系统调用表:存放系统调用程序的入口地址
5.1 参数传递问题:用户程序的参数----------->内核系统调用函数
- 陷入指令自带参数:陷入指令要带系统调用号,所以参数太多的情况带不了怎么办?
通过寄存器传递参数:使用通用寄存器,寄存器的数量通用有限,不过多数情况能满足,如果是在不能满足,在内存中开辟专用堆栈区,用来参数的拷贝。
6. 内核系统调用表长什么样子
//linux-2.6.27.10
#define _ALPHA_UNISTD_H
#define __NR_osf_syscall 0 /* not implemented */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_osf_old_open 5 /* not implemented */
#define __NR_close 6
#define __NR_osf_wait4 7
#define __NR_osf_old_creat 8 /* not implemented */
#define __NR_link 9
#define __NR_unlink 10
#define __NR_osf_execve 11 /* not implemented */
#define __NR_chdir 12
#define __NR_fchdir 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_chown 16
#define __NR_brk 17
#define __NR_osf_getfsstat 18 /* not implemented */
#define __NR_lseek 19
#define __NR_getxpid 20
#define __NR_osf_mount 21
#define __NR_umount 22
#define __NR_setuid 23
#define __NR_getxuid 24
#define __NR_exec_with_loader 25 /* not implemented */
#define __NR_ptrace 26
注:以上纯属个人见解,如有不恰当的地方,希望不吝赐教。