linux系统调用理解之摘录(2)
原文博客 http://blog.****.net/gatieme/article/details/50779184
Linux系统调用的实现机制分析
本文介绍了系统调用的一些细节。
首先,分析了系统调用的意义,他们与库函数和应用程序接口的关系。
然后,我们分析内核如何实现系统调用,以及执行系统调用的连锁反应:
陷入内核——>传递系统调用号和外部输入参数——>执行对应的系统调用函数——>把返回值带回用户空间。
最后,分析如何增加系统调用,并提供从用户空间访问系统调用的例子;
1.系统调用过的意义
Linux内核中设置了一组用于实现系统服务的子程序,这些程序成为系统调用(程序)。注意:请自行根据上下文理解“系统调用”指的是一种操作或是具体的子程序。
系统调用和普通函数调用非常类似,只是系统调用(这里指的是子程序)是由操作系统核心提供,运行在内核态,而普通的函数调用由用由函数库或用户自己提供,运行在用户态。
一般,进程是不能访问内核的:不能访问内核空间,也不能调用内核函数。这是由CPU硬件决定的(这就是为什么它被称为“保护模式”)。为了和用户空间上的进程进行交互,内核提供了一组接口,即系统调用。通过接口,应用程序可以访问硬件设备和其他操作系统资源。
系统调用相当于在用户空间和硬件设备之间添加了一个中间层。它的主要作用有三个:
(1)为用户空间提供一个统一的硬件的抽象接口。比如,当需要读取文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用管文件所在的文件系统是哪种类型,直接通过接口就能达到读文件的目的。
(2)系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则,对需要进行的用户程序请求进行裁决。比如,这样可以避免应用程序不正确地使用硬件设备,或是窃取其他进程的而资源,或是做出危害系统的事情。
(3)每个进程都运行在虚拟系统中,而在用户空间和系统的其他部分之间增加一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,那就没法实现多任务和虚拟内存。
(迷糊??)
在Linux中,系统调用时用户空间访问内核的唯一手段;除异常和中断外,系统调用时内核唯一的合法入口。
2.API/POSIX/C库的关系
一般情况下,应用程序通过应用程序接口(API)而不是使用syscall来实现系统调用。
这点很重要,因为应用程序使用API接口实际上并不需要和内核提供的系统调用一一对应。一个API可以通过一个系统调用实现,也可以通过使用多个系统调用来实现,甚至不适用任何系统调用也是可以的。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全一样的接口,但是在不同系统上,他们的内部实现可能是不同的(比如通过 ifdef 来区分)。
在UNIX中,最流行的API是基于POSIX标准,其目标是提供一套基于unix的可移植操作系统标准。
POSIX是说明API和系统调用之间关系的一个极好的例子。在大多数Unix系统上,根据POSIX标准定义的API函数和系统调用之间有直接的关系。
Linux的系统调用与大多数Unix系统一样,作为C库的一部分提供,如下图所示。C库实现了Unix系统的主要API,包括标准应用层的库函数和系统调用封装函数。所有的C程序员都可以使用C库。
从程序员的角度看,系统调用无关紧要,他们只需要和API打交道。相反,内核只跟系统调用打交道;
关于Unix的界面设计有一句通用的格言“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定目标的函数。至于这些函数怎么用完全不需要内核去关心。区别对待机制(mechanism)和策略(policy)是Unix设计的一大亮点。大部分编程问题都可以被分割成两部分:“需要提供什么功能(机制)”和“怎么实现这些功能(策略)”
(不明觉厉。。。)
3.系统调用的实现
您或许疑惑:“当输入cat proc/CPUinfo时,cupinfo()函数怎么如何被调用的?”
实际上,内核在完成引导后,控制流就从相对之间的“接下来调用哪个函数?”改变成为“等待模式”:等待系统调用、异常和中断。
用户空间的程序无法直接执行内核代码,而是以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,并执行那里的异常处理程序。
通知内核的机制是靠软中断实现的。过程如下:
首先,用户程序设置系统调用号和外部输入参数;
然后,应用程序执行“系统调用”指令(特殊的机器指令,在x86上是:“INT $0x80”,)。
在x86上,这个指令:产生一个编号为0x80的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call()(别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。
system_call()的主要作用:
a、保存程序的现有状态,即进程在用户态下的CPU主要寄存器的值(所以叫软中断)(???有问题)
b、根据系统调用号计算出应该使用哪一种系统调用,内核进程查看系统调用表sys_call_table找到对应的系统调用服务例程的入口地址;
c、转到对应的系统调用服务例程,并进一步调用执行内核中的相关功能函数;
d、上述系统服务例程执行完成后,返回系统调用返回值。
e、恢复用户程序状态,将控制权交给应用程序。
(注意:bcd没有问题,ae的表述有问题。。)
3.2系统调用号
在linux中,每一个系统调用都会被赋予一个系统调用号。
同时,Linux有一个“未实现”系统调用sysy_ni_syscall(),它除了返回ENOSYS外,不做任何工作,这个错误号就是专门为无效的系统调用设定的。
内核中所有已经注册过的系统调用都会保存在sys_call_table表中。一般在entry.s中定义。
sys_call_table是一张由指向实现各种系统调用的系统服务例程的函数指针组成的表。
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open) /* 5 */
.long SYMBOL_NAME(sys_close)
.long SYMBOL_NAME(sys_waitpid)
。。。。。
.long SYMBOL_NAME(sys_capget)
.long SYMBOL_NAME(sys_capset) /* 185 */
.long SYMBOL_NAME(sys_sigaltstack)
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
(还是不明白,这里面SYMBOL_NAME作用是?sys_vfork的宏定义是??)
system_call()函数通过将给定的系统调用好与NR-syscall作比较来检查器有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。
call *sys_call-table(, %eax, 4)
由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位
3.3 参数传递
除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。
给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。接下来许多关于系统调用处理程序的描述都是针对x86版本的。但不用担心,所有体系结构的实现都很类似。
3.4 参数验证
系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。
最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:
指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。
如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。
3.5 内核空间与用户空间之间数据的传递
内核提供了2种方法来实现用户空间和内核空间之间数据的来回拷贝。
(1)向用户空间写入数据:copy_to_user()函数
(2)从用户空间读数据:copy_from_user()函数
注意copy_to_user()和copy_from_user()都有可能引起进程阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
3.6 系统调用的返回值
系统调用(在Linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。如果一个系统调用失败,你可以读出errno的值来确定问题所在。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。
errno不同数值所代表的错误消息定义在errno.h中,你也可以通过命令"man 3 errno"来察看它们。需要注意的是,errno的值只在函数发生错误时设置,如果函数不发生错误,errno的值就无定义,并不会被置为0。另外,在处理errno前最好先把它的值存入另一个变量,因为在错误处理过程中,即使像printf()这样的函数出错时也会改变errno的值。
当然,系统调用最终具有一种明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:
asmlinkage long sys_ getpid(void)
{
return current-> tgid;
}
上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则
4.添加新的系统调用
给Linux添加一个新的系统调用是相对容易的工作。怎么设计和实现一个系统调用是难题所在,而把它添加进内核的过程比较简单。
在添加一个系统调用是我们需要考虑几个问题:
(1)明确系统调用的用途。
注意:Linux不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择不同类别的功能),不要让一个系统调用太复杂!
但是,这里有一个反例,ioctl()系统调用(可以查看详细教程https://blog.****.net/zifehng/article/details/59576539)
(2)确定系统调用的参数,返回值和错误码。
系统调用的接口应该尽量简洁,设计越通用约好。这个系统调用可移植吗?别对机器的字节长度和字节序做假设。当你写一个系统调用的时候,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。
当编译完一个系统调用后,把它注册成一个正式的系统调用是一件琐碎的工作,有如下:
(1)在系统调用表的最后添加一项。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始算起,系统调用在该表中的位置就是它的系统调用号。(这一点非常重要,在表中并不会出现具体的数值号)
(2)对于各种体系结构,系统调用号必须定义在<asm/unistd.h>中。
(3)系统调用必须编译进内核映像中(不能编译成模块)。可以通过把它放进kernel/下的一个相关文件中就可以。或是自己定义一个文件,并被包含编译(这样比较麻烦)。
以下:
我们通过虚构一个系统调用f00()来观察一下这些步骤。
(1)首先,将sys_f00加入系统调用表中,对于大多数体系结构来说,sys_call_table表位于entry.s文件中,形式如下:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open) /* 5 */
......
我们将新的系统调用添加在表的尾行:
.long SYMBOL_NAME(sys_f00)
虽然,这里没有明确指明系统调用号,但我们加入的这个系统调用被按照次序分配给了283这个系统调用号!
对于每种需要支持的体系结构,我们必须将自己的系统调用添加到其sys_call_table中。(说明表不止一个,每种体系都有一个)
(2)将自己的系统调用号加入<asm/unistd.h>中。
它的格式如下:
/*本文件包含系统调用号*/
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
..................
然后,我们再该列表的加入自己的系统调用号
#define __NR_f00 283
(3)f00系统调用的函数实现。
因为f00系统调用要被编译进内核映像,因此我们将它写进 kernel/sys.c 文件中。
asmlinkage long sys_f00(void)
{
return 1;
}
这样严格来说,现在就可以在用户空间调用f00()系统调用了。