Linux Kernel Development 笔记(四)系统调用

现在的操作系统,都会提供一套可以让用户态的进程用来与系统交互的接口。该接口给予应用受控的硬件访问能力,创建新进程以及与之沟通的能力,以及获取系统其他资源的能力。这些接口就像用户态与内核态的信使,应用借着这些信使来向内核提出请求,而内核则满足这些请求。系统调用提供了用户空间进程与硬件之间的一个中间层,主要满足了三个目的:

1. 给用户态的进程提供了一个抽象的硬件接口,让用户可以在不用知道具体的硬件信息情况下可以直接访问以及操作硬件(如读文件,可以不用管媒介,类型,文件系统等)

2. 系统调用保证了系统的安全以及稳定。由内核中间这一层,可以对访问根据授权,用户以及其他条件给予仲裁。

3. 介于用户以及剩余系统的一个公共层,可以让系统对进程提供虚拟的系统。(用户都统一调用此接口,故此可以针对此接口开发虚拟的系统而非真实的硬件系统)

系统调用,是唯一合法的提供给用户访问内核的接口。有趣的是,Linux提供的系统调用接口远少与其他操作系统。

一般来说,应用都是基于API(Application programming interface)来创建的,而不是系统调用。API与内核系统调用没有必然的联系,API可以有一个系统调用或多个甚至完全没有系统调用。还有一个更通用的API是基于POSIX标准。Linux是尽可能的往POSIX以及SUSv3标准靠近。Linux如大多数Unix系统一样,其系统调用接口是由部分的C库提供的。C库主要提供了Unix的大部分API以及标准的C库还有系统调用。从应用程序员的观点来看,系统调用是不相干的,他们只在乎API。相反,内核主要关注系统调用,而不管这些系统被谁使用。但是,内核跟踪系统调用的潜在使用以及保持系统调用的灵活性是很重要的。系统调用是通过定在在C库中的函数调用来访问的。系统调用一般情况下会返回一个long型,代表成功与否。一般0代表成功,非0代表错误。在C库中,当调用返回错误时候,会把错误码填入全局变量errno,这个变量的值的含义可以由perror函数来诠释。

系统调用都有定义好的行为,内核必须给系统调用定义一种行为目的,但不限制其采用何种方式来实现,只要最后结果是正确的。一般的系统调用定义如下:

asmlinkage long sys_getpid(void),asmlinkage是告知编译器,函数的参数是存放在栈里,这是系统调用需要的。函数返回的是long,在内核空间里是long,但在用户空间返回的是int。同时,在定义函数的时候,如果需要调用getpid,则定义sys_getpid,在前面要增加sys,这是所有linux系统调用的传统。每一个系统调用都赋予一个syscall数字。这数字对于一个系统调用是唯一的,标明了系统调用可以使用。进程引用系统调用的时候,不是引用名字的。syscall很重要,一旦分配则不能更改,否则应用会被破坏。同时,就算系统调用被移除,这个数字也不能被回收,否则已经编译好的代码会引用错误的系统调用。Linux提供了一个没有任何实现的函数 sys_ni_syscall,仅仅是返回-ENOSYS。主要是被用来补救而用,以防罕见的比的系统函数被移除或别的原因不复存在了。系统调用与syscall数字的对应是放在一个sys_call_table的表里,表给系统调用都分配唯一的syscall数字。系统调用的速度是很快的,得益于Linux系统的上下文切换的高效,这使得进入退出内核是水到渠成的事情。用户态程序不能直接的调用内核态函数,必须通过某种信号的方式来触动系统调用。这种方式就是触发一个软中断:产生一个exception,系统会切换到内核状态,执行exception处理函数,这个函数会具体的调用要调用的系统调用函数。在触发例外处理函数时候,syscall数字会被传给内核,system_call函数会比较这个数字与NR_syscalls,如果比它大则返回错误。否则调用sys_call_table来调用syscall数字对应的系统调用。


这是一个调用的例子,应用调用read的C库函数,接着通过system_call进入内核,调用实际的系统调用sys_read

系统调用的行为不怎么关心其实现,因此往linux添加一个系统调用是很简单的事情,最难的部分在于系统调用的设计与实现。

系统调用的设计必须要考虑其实现的目的,一个调用实现多种目的是不被建议的,ioctl其实不是一种很好的设计。系统调用设计尽量包含最少的参数。系统调用的语义以及行为很重要,必须保证不改变,因为众多应用都依赖于它。设计的时候,要提前考虑:函数后续会如何变化,新的功能能否添加进系统调用?或者需要新的系统调用?你能很容易的在不破坏兼容性的情况下修改问题?多数系统调用都带有一个标志参数来保证未来的扩展兼容。这个标志并不是为了实现一个系统调用实现多种行为而存在的,只是为了将来增加新功能的时候不会破坏向后兼容或需要增加新系统调用的需要。设计前瞻性的系统调用很重要,不要过多的限制函数,尽量设计的简洁。不要假设今天能用的在明天也一样用。系统调用的目标是尽量保持固定但其作用或许会变化。一句格言:provide mechanism, not policy(提供能力,但不限制用法)。当你提供系统调用,你必须实现可移植性以及健壮性,不仅适用与今天,也适用与未来。Unix很多系统调用,至今还是一样跟30年前有用。

系统调用必须非常小心的检查参数的合法性以及有效性。因为系统调用是跑在内核态的程序,非法的参数很容易破坏内核的安全性以及稳定性。举个例子,文件的读写,系统调用必须检查文件描述符是否正确,进程相关的函数必须检查PID是否有效,给予访问对应的文件等。一个最重要的检查就是检查用户提供的指针有效性。如果没有这样的检查,进程会诱发内核去拷贝它们无法访问的数据,如另外进程的数据或标志为不能读取的数据。因为,在指针回到用户态时,系统要检查:

1. 如果指针指向用户态的内存空间,进程必须不能欺骗内核获取内核空间的数据

2. 如果指针指向进程的地址空间,进程必须不能欺骗内核访问其他人的数据

3. 进程不能忽略内存访问的限制直接读取不能读的,写不能写的,执行不能执行的区域。

内核提供了两个方法来执行必备的检查以及必要的数据拷贝或写入用户空间,内核必须不能盲目的把指针流向用户态。要写数据到用户态空间,调用copy_to_user。要读取用户态空间数据,调用copy_from_user。

最后要检查的一项是权限的有效性。Linux系统允许对特殊的资源进行访问测试,通过调用capable带上一个有效的能力标志。返回非零,则意味具有着特殊的能力。0则不具备。举个例子:capable(CAP_SYS_NICE)检查进程是否具备修改其他进程nice的能力。默认情况下,超级进程拥有所有能力。

系统调用是出于调用进程的上下文情境中,也就是说在系统调用中current宏指向的是当前调用这个系统调用的进程。在进程的上下文中,系统调用在内核中,是可以休眠或被抢占的,这两点很重要。这意味着系统调用可以使用主要的内核函数。被抢占意味着系统调用必须是可重入的。当系统调用结束后,会从system_call返回到用户态并继续执行。在系统调用设计完成后,是很容易被注册为一个合法的系统调用,只要:

1. 在系统调用的表末尾增加一个入口。syscall数字是从0开始的。

2. 针对每一个架构(CPU)定义对应的syscall数字。

3. 把syscall编译进内核映像中去。

举个例子,foo系统调用,大部分架构的系统调用表是位于entry.s文件里的:

ENTRY(sys_call_table)

......

则在最后,把sys_foo加上。

然后数出sys_foo对应的次序,也就是syscall数字,并把这个记录在unistd.h文件里,如#define __NR_foo 338

这样foo系统调用就实现了。

一般说来,在用户态调用系统调用中,标准C库提供对系统调用的支持。用户可以在自己的代码里面配合C库的函数来使用系统调用。但如果自己写的系统调用,或许会被怀疑是否glibc已经实现了相同的函数名。还好,Linux提供了一个宏来包装访问系统函数。这个宏是_syscaln(),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+2×n个,头两个是返回类型以及函数名。接着依次是参数类型以及参数名。这个宏扩展成C函数以及内嵌汇编,就会把syscall数字以及对应的参数填进对应的寄存器,并触发一个软中断来转到内核状态执行。跟着,在应用那里只需要调用open即可。虽然增加一个系统调用很简单,但不鼓励这样做。因为增加一个新系统调用需要十分的小心以及受限很多,同时更多可替代添加系统调用的选择存在。

选择增加系统调用的好处:

1. 系统调用实现简单,使用方便

2. 系统调用执行效率高

缺点:

1. 必须官方的分配一个系统调用对应的syscall数字

2. 一旦系统调用发布在稳定的内核中,系统调用的接口再也不能更改

3. 每一种cpu架构的linux都要实现一遍

4. 不能简单的从脚本中调用以及通过文件系统方式访问

5. 因为要分配syscall号码,故此这个号码在内核主干树之外难以维护

6. 如果仅仅是为简单的数据交换目的,系统调用有点过了。

可选择非系统调用方式:

1. 实现一个设备驱动以及对应的读写接口,使用ioctl来处理特殊的设定以及获取指定的信息。

2. 某些接口,如信号量,可以用文件描述代表以及同样方式操作。

3. 在sysfs的适当位置增加一种文件来获取信息。

避免在Linux增加新的系统调用,有助于Linux内核的简洁以及稳定。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值