第五章 系统调用
一、与内核通信
系统调用在用户控件进程和硬件设备之间添加了一个中间层,作用如下“
- 为用户空间提供了一种硬件的抽象接口
- 系统调用保证了系统的稳定和安全
- 每个进程都运行在虚拟系统中,而在用户控件和系统的其余部分提供这样一层公共接口
在Linux中,系统调用是用户控件访问内核的唯一手段;除异常和陷入外,他们是内核唯一的合法入口。
※系统调用与API的不同在于运行级别的切换。
二、API、POSIX和C库
POSIX、API、C库和系统调用之间的关系如下图:
POSIX-Unix世界中最流行的应用编程接口是给予POSIX标准的。
Linux的系统调用作为C库的一部分提供。
C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口,即POSIXdM大部分API。
Unix的接口设计——“提供机制而不是策略”
三、系统调用
系统调用- syscall
通常通过C库中定义的函数调用来进行系统调用。
几种常见机制:
- 通过返回一个long型的返回值来表示成功或者错误
- 用一个负的返回值来表明错误
- 返回0代表成功
- 系统调用出现错误的时候会把错误码写入errno全局变量
- 通过perror()库函数可以把该变量翻译成用户可以理解的错误字符串
定义系统调用:
例:asmlinkage long sys_getpid(void)
1. asmlingkage,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。
2. 返回值long。
为了保证32位和64位系统的兼容,系统调用在用户空间返回值int,内核空间long
3. 命名规则:sys_xxx
1.系统调用号
每个系统调用号独一无二,一旦分配就不能再有任何变更。
执行系统调用时,通过系统调用号指明,进程不会提及系统调用的名称。
未实现系统调用——sys_ni_syscall(),返回-ENOSYS,针对无用的系统调用。
2.系统调用的性能——执行速度快
原因?
- 上下文切换时间短
- 系统调用处理程序和每个系统调用本身都很简洁
四、系统调用处理程序
因为用户控件不能直接执行内核代码,需要切换内核态,它需要用某种方式告知内核,自己需要执行一个系统调用,请求切换到内核态,这个通知内核的机制是软中断。
——通过引发一个异常来促使系统切换到内核态中去执行异常处理程序,即系统调用处理程序system_call()。
中断号128,指令如下:
int 128
或者
int 0x80
退出是iret
※新指令sysenter,比int中断指令更快更专业。
1.指定恰当的系统调用
2.参数调用
这两节讲述的内容在视频中亦有体现,不在此重复。
新知识点:
system_call()函数通过将给定的系统调用号与NR_syscalls作比较来检查其有效性。大于等于返回-ENOSYS,否则执行相应系统调用:
call *sys_call_table(,%rax,8)
过程:
int 0x80 → 中断向量表中找到对应中断向量 →系统调用表,
JMP(EAX*4 + system_xxx)
乘以4是因为系统调用表中的表象是以64位存放的。
五、系统调用的实现
1.实现系统调用
- 决定它的用途
不提倡采用多用途的系统调用 - 确定参数、返回值、错误码
力求简洁 - 越通用越好,不做错误假设
- 时刻注意可移植性和健壮性
2.参数验证
必须检查每个参数,保证他们不但合法有效,而且正确。
最重要——检查用户提供的指针:
- 指针指向的内存区域必须属于用户空间
- 指针指向的内存区域在进程的地址空间内
- 决不能绕过内存访问限制
内核无论何时都不能轻率地接受来自用户空间的指针。
※1.检查读写
(1)向用户空间写入数据——copy_to_user()
参数:
进程空间中的目的内存地址
内核空间内的源地址
需要拷贝的数据长度
(2)从用户控件读取数据——copy_from_user()
把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上
第三个参数——拷贝数据长度。
以上两个函数成功返回0,失败返回没能完成拷贝的数据的字节数。
这两个函数都有可能引起阻塞——当包含用户数据的页被换出到硬盘上而不是物理内存上的时候。
※2.检查是否有合法权限
老版本:suser()
新版本:权能机制,capable(),返回非0有权操作,返回0则无权操作。
六、系统调用上下文
内核在执行系统调用时处于进程上下文。
在进程上下文中,内核可以:
- 休眠
说明系统调用可以使用内核提供的绝大部分功能 - 可以被抢占
要求保证该系统调用是可重入的
1.绑定一个系统调用的最后步骤
1,在系统调用表的最后加入一个表项。
2,对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中
3,系统调用必须被编译进内核映像,不能被编译成模块。——放进kernel/下的一个相关文件中即可,例如sys.c。
2.从用户空间访问系统调用
Linux本身提供了一组宏,用于直接对系统调用进行访问。
_syscalln() //n的范围从0到6,代表需要传递给系统调用的参数个数。
例如:
long open(const char *filename, int flags, int mode)
=
#define NR_open 5
_syscall3(long, open,const char*, filename, int, flags, int, mode)
应用程序可以直接用open()了。
对于每个宏来说,都有(2+2xn)个参数:
1.系统调用的返回值类型
2.系统调用的名称
以后按照系统调用参数的顺序排列每个参数的类型和名称。
_NR_open在<asm/unistd.h>中定义。
这个宏会被扩展成为内嵌汇编的C函数。
3.为什么不通过系统调用的方式实现
好处:
- 系统调用创建容易并且使用方便
- linux系统调用的高性能
问题:
- 占用系统调用号
- 固化,不允许改动接口
- 需要分别注册到每个需要支持的体系结构中
- 脚本中不易调用,文件系统中也不能直接访问
- 在主内核树外难以维护使用
- ……
替代:
- 某些接口,例如信号量,用文件描述符表示
- 把增加的信息作为一个文件放在sysfs的合适位置。
总而言之——
Linux尽量避免每出现一种新的抽象就简单地加入一个新的系统调用。
七、 学习心得
这一章前半部分在视频中学过,因此学习起来毕竟轻松,我也省略了一些重复的知识以精简这篇学习笔记;而后半部分目测是下周的内容,光从书本上学习没有视频直观,老师上课也补充了一部分,我会在下周的课程中继续认真学习。