系统调用
1.概述
系统调用是内核与用户进程进行交互的一组接口,这些接口让应用程序受限制的访问硬件设备,提供了创建新进程并与已有进程进行交互通信的机制,也提供了申请操作系统其他资源的能力。
系统调用在Linux系统中的地位如下图所示:
2.与内核通信
系统调用在用户空间进程和硬件设备之间添加了一个中间层。这样的中间层有如下作用:
1.为用户空间提供了一种硬件的抽象接口。
2.系统调用确保了系统的稳定与安全。内核可以作为硬件设备与用户空间之间的中间人,基于权限、用户类型和一些其他的规则对需要进行的访问做出裁决。
3.避免用户空间的应用程序对硬件资源随意访问而内核一无所知,这样既无法实现多任务和虚拟内存,也不能确保良好的稳定性和安全性。
Linux中:系统调用是用户空间访问内核的唯一手段—-除了异常和陷入外,它们是内核唯一的合法入口
3.API、POSIX、C库
应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用。
1.API实际上并不需要与内核提供的系统调用一一对应,程序员只跟API打交道。
2.内核只跟系统调用打交道,库函数以及应用程序如何使用系统调用不是内核所关心的。
3.API可以实现成一个系统调用,也可以实现多个系统调用,甚至也可以不实现系统调用。
在UNIX中,最流行的应用编程接口是基于POSIX标准的。
Linux系统调用作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口,此外,C库提供了POSIX的绝大部分API。
4.系统调用(syscall)
通过在C库中定义的函数调用来执行
系统调用在出现错误的时候C库会将错误码写入errno全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。
为了保证32为操作系统和64位操作系统的兼容,系统调用在用户空间和内核空间有着不同的返回值类型。用户空间为int,内核空间为long。系统调用在内核中前面会加上sys_。例如:返回当前进程PID的系统调用get_pid()在内核中被定义为sys_getpid()。
4.1系统调用号
在Linux中,每个系统调用都会被赋予一个系统调用号(独一无二的)。进程在执行系统调用时,不需要提及系统调用的名称,只需要使用系统调用号即可。
系统调用号相当重要,一旦分配好就不能再更改,因为会导致之前编译好的应用程序崩溃。即使这个系统调用被删除,它所占用的系统调用号也不允许被回收利用。
内核记录了系统调用表中的所有已经注册过的系统调用的列表,存储在sys_call_table中,这个表为每一个有效的
系统调用指定了唯一的系统调用号。
4.2系统调用的性能
Linux系统调用比其他许多的操作系统执行要快。原因如下:
1.Linux很短的上下文切换时间,进出内核都被优化的极为高效。
2.系统调用处理程序和每个系统调用本身非常简洁。
5.系统调用处理程序
应用程序通过软中断的方式来告诉内核自己期望执行一个系统调用,希望系统当前可以从用户态切换到内核态,这样内核就可以代表应用程序在内核空间中执行系统调用。
软中断:通过引起一个异常来促使系统切换到内核态前去执行异常处理程序。此时的异常处理程序其实就是系统调用处理程序。
在X86系统中预定义的软中断是中断号128,通过int $0x80来触发中断。这条指令会触发一个异常导致系统切换到内核态并执行128号异常处理程序(系统调用处理程序)。
5.1指定恰当的系统调用
在陷入内核空间时,要将系统调用号传给内核空间。X86系统,系统调用号是通过eax寄存器传递给内核的。system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。大于或者等于NR_syscalls,函数就返回-ENOSYS,否则,执行相应的系统调用。
系统调用表中的表项是以64位类型存放的,所以内核需要将给定的系统调用号乘4,然后用所得结果在表中查询位置。
5.2参数传递
部分系统调用除了系统调用号之外,还需要传入一些外部参数,在发生陷入的时候,一并传给内核,同样是放在寄存器中。给用户空间的返回值同样是通过寄存器来传递的
6.系统调用的实现
6.1实现系统调用
要求:
- Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来完成选择的工作)
- 系统调用的接口应该力求简洁,参数尽量少。提供标志参数以确保向前兼容。
- 系统调用的设计越通用越好。
6.2参数验证
系统调用必须检查参数是否合法有效。系统调用在内核空间中执行,任由用户将不合法的输入传递给内核,系统大的安全和稳定性将面临考验。进程不能让内核访问那些他没有权限访问的数据。
- 与文件IO有关的系统调用需要检查文件描述符是否有效,与进程有关的函数必须检查提供的PID是否有效。
- 检查用户提供的指针是否有效
- 指针指向的内存区域只能属于用户空间,进程不能告诉内核去读内核空间中的数据。
- 指针指向的内存区域在自己的进程中,不能让内核去读其他进程中的数据。
- 进程不能越过内存访问限制(可读、可写、可执行)
- 检查是否有合法权限
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间的数据来回拷贝
- 写入:copy_to_user()
- 读取:copy_from_user()
这两个方法可能会引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,进程会休眠,直到缺页处理程序将该页从硬盘从新换回物理内存。
7.系统调用上下文
在进程调用系统调用时,会有进程上下文。既可以被抢占也可以休眠。
可以休眠说明系统调用可以利用内核提供的绝大部分功能。可以被抢占说明该系统调用时可重入的(新的进程同样可以使用相同的系统调用)。
系统调用返回时,控制权仍然在system_call()中,它最终会负责切换到内核工作空间,并让用户进程继续执行下去。
7.1绑定一个系统调用的最后步骤
1.在系统调用表中的最后加入一个表项。从0开始算起,系统调用在表中的位置就是他的系统调用号。
2.系统调用必须被编译进内核映像(不能被编译成模块)
7.2从用户空间访问系统调用
系统调用靠的时C库的支持,用户程序通过包含标准头文件并和C库链接,就可以使用系统调用。
Linux新系统调用增添频率很低,说明Linux系统是一个相对较为稳定并且功能已经较为完善的操作系统