应用程序必须通过系统调用来访问系统资源(软的,硬的),这是提供一个稳定系统的关键。
系统调用可以认为是位于硬件和用户空间进程之间的一层,主要有3个作用:
1.为用户空间提供一个抽象硬件接口。比方说操作文件,用户进程调用open,read,write,close,seek等系统调用,一个用户进程调用系统调用之后,这个时候的内核就是executing on behalf of a process,而用户进程是不允许直接操作硬件的,比方说像dos那样直接读取某个扇区。
2.系统调用保证了系统的安全性和稳定性。内核可以根据资源的访问限制来决定某进程对资源的访问方式,包括拒绝访问,像什么破坏系统,窃取别人的资源等等。
3.为进程提供了一个虚拟系统的概念,主要指的就是多任务和虚拟内存。
系统调用、异常、陷阱(某些书上把系统调用和中断归入到陷阱中,概念上的事情向来扯不清楚,知道机制是类似的就可以了)是用户进程进入内核空间的合法入口。中断是由设备发出的,与系统调用隔(内)核相对。
Linux的系统调用相对不多,x86上大概是250个(也不少了).
系统调用不是API,应用程序通过API来编程而不是系统调用。API甚至可以不使用系统调用。POSIX就是API的集合,大部分都是c函数。
系统调用是有确定功能的函数,有参数,有返回值,有些会改变系统状态,有些不会。
看这个函数
asmlinkage long sys_getpid(void)
{
Return current->tgid;
}
getpid()返回当前进程的Pid,这是个不改变系统状态的系统调用,asmlinkage修饰符表示编译器要从栈中获取这个函数的参数,所有的系统调用都是如此,并且一个系统调用在内核中的定义总是需要加一个sys_前缀。
系统调用有一个唯一的编号,称为syscall number,也就是所谓的系统调用号。系统调用号一旦确定就不可更改,并且即便被移除,这个号码也不可回收只能留空。注册的系统调用保存在一个叫sys_call_table的列表中。这个表是依赖于架构的,通常在entry.S中定义,x86中这个文件位于arch/i386/kernel目录下。
Linux提供一个sys_ni_syscall()的系统调用,这个调用只返回-ENOSYS,表示非法的系统调用,一般用来填空,就是如果一个系统调用被移除或者由于某种原因不可使用时,对应的系统调用号就指向这个。
用户空间的应用程序直接调用内核代码是不可能的,因为内核空间是被保护的,只能是向核发送一个信号,表示要执行一个系统调用,然后内核转入内核模式执行系统调用。
这个机制就是软中断:引发一个异常,然后系统装入内核模式并执行异常句柄,在这种情况下,这个异常句柄,就是系统调用句柄。在x86中是int $0x80指令,其实也就是80号中断,这条指令触发转向内核模式然后执行128(0x80)号异常向量,也就是系统调用的入口,这个句柄就是system_call()。
当然仅仅进入内核空间是不够的,必须要让内核知道需要执行哪个系统调用,这就是系统调用号的作用,x86中这个参数通过eax传递。System_call()要检查系统调用号是否合法,然后调用这个函数:call *sys_call_table(,%eax,4),也就是调用系统调用表偏移%eax*4那个句柄。可以看出系统调用表的每个条目是32位,这个是架构决定的。
除了系统调用号,有时候还需要传递参数,这要通过寄存器来实现,在x86中,ebx,ecx,edx,esi,edi依次存放前5个参数,如果需要6个或以上的参数,则把一个指向用户空间参数存放位置的指针放在一个寄存器中。
返回值也是通过寄存器传回,x86中是eax,eax在一个系统调用中身兼两职。
当系统调用返回时,system_call()还需要继续执行,以最终切换回用户空间并执行用户进程。
Linux中添加一个系统调用本身很简单,难点在设计和实现。
实现一个系统调用首先要定义其要做什么,一个系统调用的目的应该是单一的。至于ioctl()应当作为一个系统调用中不要作什么的例子。其次确定参数,返回值以及错误码。系统调用的接口应当是简单明确并且带尽可能少的参数。最后要着眼于未来,要注意可移植性和健壮性,unix的基本系统调用就是很好的范例。
因为运行在内核空间,系统调用必须验证校验参数有效性、合法性,还要正确。最重要的检查是用户提供的指针的有效性。在跟随一个指针进入用户空间时系统要确保:
1.指向用户空间;
2.必须是调用系统调用的进程地址空间;
3.遵守存取限制。
系统提供了两个方法来实现上述检查以及内核与用户间的数据交换:copy_to_user(),copy_from_user();这两个函数成功返回0,失败返回复制失败的字节数。当然系统调用发现这种情况后会返回-EFAULT。这两个函数都有可能堵塞,这时候一般是出现了页面失效,也就是需要访问的地址不在内存中。
最后一个检查是访问许可(valid permission),在旧版本中需要root权限的系统调用可以调用suser(),这个调用不检查用户是root用户还是什么别的,现在已经被移除,并引入一个相容性(capability)概念,通过调用capable()来检查是否允许访问特定的资源,如果返回非0允许,否则禁止。
内核在执行系统调用时处于进程上下文,current指针指向调用系统调用的那个进程。在进程上下文中,内核可以睡眠,也可以被抢占,因此要保证系统调用的可重入性,这点与中断句柄是不同的。
注册已经写好的系统调用是很简单的:
首先,在系统调用表末端加一个条目,因为linux支持很多架构,这个工作对每个支持的架构都要进行一次,系统调用在系统调用表里的位置就是其系统调用号,从0开始计数。系统调用表一般在entry.S中,如前所述,每个架构都要加,不过不同的架构同样的调用系统调用号无需一致
最后,系统调用需要编译进内核映像中而不是编译成模块,这个不难理解吧,其实很简单,把系统调用放到kernel/中的一个合适的文件中就好了,比如说sys.c,这是系统调用的大本营,当然也可以放到别的地方,比方说跟调度有关的可以放到kernel/sched.c中。
C库一般都支持系统调用,用户程序可以包含标准库的头文件然后和C库链接来使用你的系统调用,或者使用你的系统调用的库函数。但如果是你写的系统调用,那么glibc是否已经支持就不确定,也就是说不一定能够在程序中直接调用你的系统调用。Linux为此提供了一个宏_syscalln(),n对应传给相应的系统调用的参数的个数,从0到6,这些参数是需要放入寄存器的。比方说long open(const char *filename,int flags,int mode),如果不用系统库支持而是直接使用,则需要使用_syscalln():
#define __NR_open 5
_syscall3(long,open,const char *,filename,int,flags,int,mode)
然后在应用中就可以直接使用open()。
这个宏在一个c函数中展开成Inline的汇编代码,所做的工作就是把系统调用号和参数放入适当的寄存器中,然后发出软中断陷入内核。把这个宏放在一个应用程序中就是使用一个系统调用的全部工作。
虽然系统调用很容易实现并且很容易使用并且运行起来非常快,但是如果你要自己去写系统调用,那么:
1.你需要一个官方给定的系统调用号;
2.一个稳定版本中的系统调用是不允许再更改的;
3.每一个架构都需要分别注册;
4.在脚本中使用系统调用是不容易的,并且不能直接通过文件系统访问;
5.对于简单的信息交换,使用系统调用太过分了。
可以使用别的方法来解决你本来打算用系统调用实现的问题:
1.实现一个设备结点,使用read()和write(),以及ioctl();
2.一些接口比方说信号量,可以由一个文件描述符表示,并且可以当成文件来使用;
3.把信息当成文件放到系统文件系统的合适位置。
总而言之,对于你自己的大多数应用,首先考虑使用文件系统来实现而不要轻易的试图自己去写系统调用。linux在添加系统调用上是非常保守的,当然要是写一个系统调用自己玩,那就随意。
阅读(1125) | 评论(0) | 转发(0) |