Linux 系统调用

系统调用
在现在操作系统中,内核提供了一系列的接口给用户空间,让这些进程可以跟内核进行交互。这些接口给给应用了一种access,可以访问到硬件;利用这种机制,可以创建新的进程、跟已知的进程进行通信、有能力申请其他的操作系统资源。这些接口就像是内核和应用之间消息的传递者。这些接口(或者说应用不能直接访问他们任意访问的资源)的存在的目的就是提供一个稳定的系统。

5.1 Communicating with kernel
系统调用提供了一个中间层,介于硬件和用户进程之间。这一中间层主要有三个目的:首先,它为用户空间提供了抽象硬件层。For example,用户应用读写文件的时候,他不需要关注磁盘类型、media甚至是文件系统类型也不需要关注。Second,系统调用保证了系统的安全性和稳定性。当内核作为一个中间层的介于系统资源和用户空间的时候,内核可以基于权限、用户和其他规则对这中间的行为做仲裁。For example,这种仲裁阻止应用不正确的使用硬件,窃取其他进程资源,或者说攻击系统。Finally,基于用户空间和系统调用之间的中间层给进程提供了一个虚拟化的系统。如果进程可以任意访问系统资源,没有得到系统确认的话,那几乎就不可能完成多任务和虚拟内存,并且也一定不可能完成系统的稳定性和安全性。在Linux中,系统调用是用户空间拥有的唯一的和内核通信的手段;他们也是除了exception和traps以外,唯一的合法进入内核的入口。事实上,其他的接口也是通过系统调用来实现的,比如说设备文件或者/proc。有意思的是,相比于其他系统而言Linux完成的系统调用少之又少。这一章我们主要讨论系统调用和系统调用的实现机制。

5.2 APIS,POSIX,and the C library
通常来说,应用通常来说是参考API来进程编程实现的,而不是直接调用系统调用接口。这很重要,因为应用使用的接口和内核提供的实际接口的非直接关联是必须的。API定义了一系列的编程接口,可供用户使用。这些接口通过一个或者多个系统调用实现,甚至不需要调用系统调用也可以实现。在不同的系统中,存在诸多的相同的API,提供给应用编程;但是不同系统的API的实现机制是各不相同的。Figure 5-1展示了POSIX API,C库和系统调用之间的关系。

在Unix世界中,应用编程更重要的一点就是基于POSIX接口标准编程。Technically,POSIX由一些列IEEE标准组成,旨在提供一个基于Unix的可移植的操作系统标准。Linux努力兼容POSIX和SUSv3.
POSIX是在API和系统调用之间关系的完美实例。在大多数Unix系统中,基于POSIX的API和系统调用有很强的合作关系。事实上,POSIX标准是用来resemble早期Unix系统的接口。换句话说,一些系统是非Unix的,比如说微软的Windows,也提供了POSIX兼容的库。
和大多数的Unix系统一样,Linux中的系统调用接口的部分是由C库提供的。C库在Unix系统中完成主要的API,包括系统调用和C库。C库是可以被所有的C语言编程所使用,基于C语言的nature,C库还可以被其他编程语言wrapped后使用到他们的编程语言中。C库额外的大多数的POSIX API。
从应用编程人员眼中看的话,系统调用是不相关的。所有的编程者关心的是API。相反地,内核仅仅关注系统调用;库调用什么和应用使用什么系统调用都不是内核所关心的。内核关注到系统调用的潜在使用方式是很重要的,而且需要尽可能保持系统调用的flexible。
Unix系统调用存在的意义是在抽象层的情况下提供一定的功能。这些功能使用的manner和内核是不相关的。
5.2.1 Syscalls
系统调用(在Linux中通常叫做syscalls)通常是由C库中的一些函数调用。他们可以定义无参数、一个或者多个参数(输入)并且可能导致一个或者多个副作用。比如说写入文件或者从提供的指针拷贝数据。系统调用也提供了一个类型为long的返回值,用来知识success或者error—通常来讲负值常常意味着出错。零值通常意味着成功。当系统调用返回一个错误的时候,C库会写入错误代码到全局的errno变量。通过库函数perror()可以把这些错误代码转换为可读的错误数值。
最终,系统调用有定义好的行为。比如说,系统调用getpid()定义的行为是返回一个inter值,指明当前进程的PID。这个系统调用的实现机制是相当简单的:
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current); // returns current->tgid
}
Note that:这项定义没解释什么完成的过程。内核必须提供一定的系统调用行为但是必须是free使用的。当然,系统调用也是需要尽可能的简单,实现他的方式并不是很多。
SYSCALL_DEFINE0仅是一个简单的宏,它定义了无参数的系统调用。The expanded代码如下:
Asmlinkage long sys_getpid(void)
让我们查看下系统调用是如何实现的。首先,注意到asmlinkage修饰符。这是直接的告诉编译器仅仅在stack上查看函数参数。这是系统调用需要的修饰符。Second,函数返回的long型参数。这是为了兼容32位和64位系统,在用户空间定义返回Int的系统调用在内核中返回一个long。Third,note that系统调用getpid()在内核重定义的是sys_getpid().这是一种系统调用的命名格式:系统调用bar()在内核中完成的话,需要定义为sys_bar()。
5.2.2 System Call Numbers
在Linux中,每一个系统调用都会分配一个系统调用number。这项参数是unique的,用来指示一个具体的系统调用。当用户空间执行一个系统调用的时候,系统调用number指出哪一个系统调用在执行。这一过程是不会利用系统调用名称来实现的。
系统调用number是很重要的;一旦被分配的话,它是不会被改变的。Likewise,如果系统调用被移除了,它的系统调用number不会被回收利用。Linux实现了一种”not implemented”系统调用,sys_ni_syscall(),它不会做其他的工作,仅仅返回-ENOSYS,指明对应的invalid系统调用。此函数用于在系统调用被删除或不可用的罕见情况下“堵塞漏洞”。
内核在系统调用列表中保存了一个已注册的系统调用的列表,存储在sys_call_table。这个列表是根据硬件架构不同而不同的,在X86-64中是定义在arch/i386/kernel/syscall_64.c。这项列表分配给每一个系统调用一个唯一的系统调用号。
Docker-ce for windows
5.2.3 System Call performance
Linux中的系统调用的速度是优于其他操作系统的。这是由于Linux的快速上下文切换时间;进入和退出内核是streamlined和简单的事务。其他的因素是系统调用handler的simplicity和系统调用本身自己的作用。
5.3 System Call Handler
用户空间应用直接执行内核代码是不可能的。由于内核是在受保护的内存空间的,他们不可能简单通过function call到内核空间的method。如果应用能够直接的读写内核地址空间的话,系统的安全性和稳定性是不存在的。
相反的,用户空间应用必须通知内核他们想执行系统调用并且将系统切换到内核模式,在内核空间中,内核会代替应用程序执行系统调用。
通知内核的机制是通过软中断的机制实现的:incur an exception.并且系统将会切换到内和模式,执行exception handler。在这种情况下,exception handler是实际就是系统调用Handler。在X86中,定义的软中断的数量是128,它可以通过int $0x80指令触发。它会触发switch切换到内和模式,exception的向量是128,which is 系统调用handler。The system call handler is the aptly named function system_call()。这是跟硬件架构相关的:在X86-84中,它是通过entry_64.S中完成的。无论系统调用handler是如何调用的,最重要的一点是用户空间如何引起exception或者traps而进入内核。
5.3.1 Denoting the Correct System Call
因为多个系统调用的存在,单一的进入一个内核空间是低效率的;所有的系统调用都是以一样的方式进入内核。因此系统调用号必须传入内核。在X86当中,系统调用号是通过寄存器eax传递给内核的。在trap into内核之前,用户空间stick在寄存器eax中的系统调用号给对应的系统调用。系统调用handler接下来会从eax中读取这项参数。其他架构的行为跟X86类似。
系统调用函数通过比较NR_syscalls和system call号来检查system call号的有效性。如果系统调用号远远大于或者等于NR_syscalls,函数就会返回-ENOSYS。否则的话,specified系统调用会被调用到:
Call *sys_call_table(,%rax,8)
由于系统调用列表中的每个参数都是64bit的(8 bytes),内核将给定的系统调用号乘4去arrive系统调用列表的位置。在X86-32中,代码是相似的,with 8 replaced by 4.see Figure 5.2.

5.3.2 Parameter Passing
除了系统调用号以外,大多数系统调用还要求传递一个或者多个参数进入系统。Somehow,用户空间在Trap的时候必须传递参数给内核。最容易实现的办法就是和系统调用号传递的方式一样,将参数存储在寄存器中。在X86-32中,寄存器ebx,ecx,edx,esi和edi顺序的包含了前五个参数。如果六个或者更多的参数的话,一个单一的寄存器就用来存储用户空间存储参数的指针位置了。
返回值后通过寄存器发送给用户空间。在X86上面,他会被写入eax寄存器。
5.4 System Call Implementation
在Linux中完成系统调用的过程是不需要关系系统调用handler的行为的。因此,添加一个新的系统调用到Linux系统中是相对而言比较容易的。真正困难的工作在于设计和完成一个系统调用;注册到内核中是比较容易的。接下来,我们介绍下在Linux系统中实现一个新的系统调用实现的步骤。
5.4.1 Implementing System Calls
完成系统调用的第一步就是定义他的目的。它将会做什么?系统调用应该确定的只有一个目的。Multiplexing syscalls在Linux中是不受推崇的(因为a single syscalls通过不同参数的输入就可以实现很多功能)。比如说,ioctl()。
新的系统调用的参数、返回值和错误代码是什么?系统调用应该尽可能设计一个clean and simple的接口,利用最少的输入参数。系统调用的机制和行为都是很重要的;他们是禁止更改的,因为现有的应用是依赖于这些的。Be forward thinking;consider how the function might change over time.新的功能能够添加到系统中吗?will any change require an entirely new function?在不破坏前向兼容的情况下,你能很容易的修复bug吗?很多的系统调用提供一个flag参数去解决forward兼容性问题。The flags is not used to multiplex different behavior across a single system call but to enable a new functionality an options without breaking backword compatibility or needing to add a new system call.
设计接口时考虑到future是非常重要的。Are you needlessly limiting the function?设计的系统调用接口应该尽可能的通用。Don’t assume its use today will be same as its use tomorrow.新的系统调用的目的是在its use改变的时候,它维持constant。Is the syscalls portable?Dont make assumptions about an architectures’s word size or endianness.Make sure you are not making poor assumptions that will break the system call in the future.Remember the unix motto:”Provide mechanism,not policy”.
当你写系统调用的时候,你需要考虑到他的可移植性和健壮性,not just today but in the future.Unix基本的系统调用已经存在了很久了;他们中的大多数到今天为止都还是useful and applicable,而这已经过去了30年了。
5.4.2Verifying the Parameters
系统调用必须仔细的检查所有的参数以确认他们是有效的合法的。系统调用运行在内核空间中,如果用户空间能够毫无限制的传递一个invalid的参数给内核,那么系统的安全性和稳定性可能就要suffered。
比如说,I/O系统调用就必须检查文件描述符是否是有效的。进程相关的系统调用必须检查提供的PID是否有效。每一个参数都必须被检查去确认参数是有效的,合法的并且正确。进程禁止向内核申请去访问进程不能访问的资源。
检查中最重要的一部分就是检查用户空间提供的指针的validity。想象一下,如果进程能够传递任意的指针参数到内核当中,Unchecked,with warts and all,甚至传递一个没有读权限的指针。进程接下来可能就会说服内核去拷贝数据,但是这些数据他是没有访问权限的,比如说属于另一个进程的数据或者data mapped unreadable。Before following a pointer into user-space,系统必须确认以下事项:
a) 指向用户空间内存区域的指针。进程禁止trick the kernel into reading data in kernel-space on their behalf.
b) 指向进程地址空间内存区域的指针。进程禁止trick the kernel into reading someone else’s data.
c) 如果reading,内存被标记为readable。如果writing,内存被标记为writable.如果execting,内存被标记为exectable。进程禁止bypass内存访问权限。
内核提供两种办法去执行必要的检查和desired copy to/from 用户空间。Note that kernel code must never blindly follow a pointer into user-space.One of the two methods must always be used.
对于像用户空间写入的话,the method copy_to_user() is provided.It takes three parameters.第一项参数是进程地址空间的目的内存地址。第二项参数是内核地址空间的source指针。最后,第三个参数是拷贝数据的size bytes。
对于从用户空间读取的话,the method copy_from_user() is provided.和copy_to_user()类似,the function从第二项参数读取到第一项参数,大小为第三项参数指定。
如果拷贝失败了的话,这两个参数都会返回一个error。如果成功了返回值为0.无论是copy_to_user()还是copy_from_user()都会block;比如说,如果包含用户数据的page不在物理内存上而是swapped to disk。在这种情况下,进程就会睡眠直到page fault handler把磁盘上的swap file重新映射到物理内存上。
最终的检查步骤是有效性检查。在旧版本的Linux中,使用suser获取root权限是标准做法。现在系统不会检查用户是否是root;现在这项功能被移除了,现在更好的”capabilities”被添加进来了。新的系统使能了specific access可以检查特定的资源。如果调用者拥有特定的capabilities调用capable(),那么返回值为非零;否则的话,返回值为零。比如说,这里有reboot()系统调用。现在第一步就是确保调用的进程拥有CAP_SYS_REBOOT.如果这项条件检查被移除了,任意的进程都能重启系统了。
5.5 System Call Context
正如第三章讨论的,内核在执行系统调用的时候是在进程上下文的。Current指针指向当前运行的任务,这就是执行系统调用的进程。
在进程上下文中,内核是可以睡眠的,也是可抢占的。这两点是很重要的。Frist,能够睡眠意味着系统调用能够利用内核的绝大多数功能。As we will see in chapter 7,”Interrupts and Interrupt Handlers”,这种睡眠的能力极大的简化了内核编程的复杂度。Secong,进程上下文是可抢占的意味着向用户空间一样,当前任务是可以被其他任务抢占的。因为新的任务执行同样的系统调用,还必须考虑到代码是可重入的。当然,这和对称多处理技术是同样的问题。Synchronizing renentrancy在第九章会提及到。
当系统调用返回的时候,control continues in system_call(),which finally switches to user-space and continues the execution of the user process.
5.5.1 Final Steps in Binding a System Call
系统调用写完之后,it is trivial to register it as a official system call.
a) 添加入口到系统调用尾。这项工作是根据硬件架构来支持系统调用,但是不同架构的实现是不一致的;系统调用在系统调用列表中的位置代表了他的系统调用号,从零开始;比如说,列表中的第十个系统调用,他的系统调用号就是九。
b) 对于每个支持的架构,定义系统调用号在<asm/unistd.h>
c) 编译系统调用到内核镜像(对应于编译为模块)。
Look at theses steps in more detail with a fictional system call,foo。If you were interested in more details of implementation,please read the original books(the linux kernel development 3.rd).
5.5.2 Accessing the System Call from User-Space
一般来说,C库支持系统调用。用户应用程序可以从标准头中提取函数原型并链接到C库以使用您的系统调用(或者库例程,它反过来使用您的系统调用)。如果你仅仅想写出一个系统调用,这感觉怪怪的,因为glibc已经支持了。
Thanksfully,内核提供了一系列的宏来访问系统调用。它设置了寄存器内容并执行trap指令。这些宏命名为_syscalln(),这里n是从0到6.n对应着传递进系统调用的参数的个数,因为这些宏需要知道它需要参数的个数并put int寄存器。比如说,系统调用open(),定义为:
Long open(const char *name, int flags, int mode)
没有具体库支持的系统调用的宏将会是:
#define _NR_open 5
_syscall3(long, open, const char *name, filename, int, flags, int, mode)
然后,应用仅仅通过open()就可以调用了。
每个宏有2+2×n个参数,第一个参数对应系统调用的返回类型,第二个参数对应系统调用的名称。接下来是每个参数的类型和名称,按照系统调用的顺序排列。The _NR_open 定义在<asm/unistd.h>中;它是系统调用号。syscall3宏使用内联程序集扩展为c函数;程序集执行上一节中讨论的步骤,将系统调用号和参数推入正确的寄存器,并发出软件中断以捕获内核。在应用程序中放置这个宏就可以使用open()系统调用了。
5.5.3 Why Not to Implement a System Call
前面的章节介绍了实现一个系统调用是很简单的,但是这并不鼓励你去这样做。事实上,you should exercise caution and restraint in adding new syscalls.通常来说,比提供一个新的系统调用更可行的办法是存在的;let’s look at the pros,cons and alternatives.
The pros of implementing a new interface as a syscall are as follows:
 系统调用是易于完成和使用的;
 系统调用在Linux中是响应迅速的;
The cons:
 你需要一个syscall number,系统调用号需要officially分配到你;
 如果系统调用是在稳定的内核系列中, 它是不可改写的;这些接口在不破坏应用程序的情况下这些是不能改变的;
 每一个不同的硬件架构都需要完成分别注册系统调用并支持他;
 系统调用在脚本中是不容易使用的,在文件系统中也不能直接访问;
 因为您需要指定的系统调用号,所以很难在主内核树之外维护和使用系统调用;
 对于简单的信息交换,系统调用是过度的;
The alternatives:
 完成设备节点,read()、write()。使用ioctl()去操作specific设置或者解析specific信息;
 一些特定的接口可以代表文件描述符并manipulated as such,比如所信号量;
 作为文件,添加信息到sysfs中合适的位置;

对于一些接口,系统调用是非常适用的。但是Linux已经尽量的避免去添加一些简单的系统调用去支持新的抽象层。系统调用添加的低速也表明Linux系统是相对稳定和特性完整的操作系统。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值