• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Linux系统原理之系统调用

系统初识 彭东稳 9年前 (2015-12-15) 31251次浏览 已收录 0个评论

系统调用

系统调用是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。例如,用户额可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件;通过时钟相关的系统调用获得系统时间或设置系统时间;通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,为了达到这个目的,所有操作系统在内核里提供一系列具备预定功能的多内核函数,这些内建函数完成对硬件的访问和对文件的打开、读写、关闭等操作。Linux系统中称这些函数叫做“系统调用”,即System Call。这些函数实现了将操作从用户空间转换到内核空间,有了这些接口函数,用户就可以方便地访问硬件了。例如,在用户空间调用open()函数,则会在内核空间调用sys_open()。系统调用把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序。一个已经安装的系统所支持的系统调用都可以在/usr/include/bits/syscall.h文件里看到。

现代的操作系统通常都具有多任务处理的功能,通常靠进程来实现。由于操作系统快速的在每个进程间切换执行,所以一切看起来就会像是同时的。同时这也带来了很多安全问题,例如,一个进程可以轻易的修改进程的内存空间中的数据来使另一个进程异常或达到一些目的,因此操作系统必须保证每一个进程都能安全的执行。这一问题的解决方法是在处理器中加入基址寄存器和界限寄存器。这两个寄存器中的内容用硬件限制了对储存器的存取指令所访问的储存器的地址。这样就可以在系统切换进程时写入这两个寄存器的内容到该进程被分配的地址范围,从而避免恶意软件。

为了防止用户程序修改基址寄存器和界限寄存器中的内容来达到访问其他内存空间的目的,这两个寄存器必须通过一些特殊的指令来访问。通常,处理器设有两种模式:“用户模式”与“内核模式”,通过一个标签位来鉴别当前正处于什么模式。一些诸如修改基址寄存器内容的指令只有在内核模式中可以执行,而处于用户模式的时候硬件会直接跳过这个指令并继续执行下一个。同样,为了安全问题,一些I/O操作的指令都被限制在只有内核模式可以执行,因此操作系统有必要提供接口来为应用程序提供诸如读取磁盘某位置的数据的接口,这些接口就被称为系统调用。当操作系统接收到系统调用请求后,会让处理器进入内核模式,从而执行诸如I/O操作,修改基址寄存器内容等指令,而当处理完系统调用内容后,操作系统会让处理器返回用户模式,来执行用户代码。

为了对系统进行保护,Linux对系统定义了内核模式和用户模式,内核模式可以执行一些特权指令并进入用户模式,而用户模式则不能进入内核模式。而用户模式则不能进入内核模式。Linux将程序的运行空间也分为内核空间和用户空间,它们分别运行在不同的级别上。在逻辑上,它们是互相隔离的。系统调用规定用户进程进入内核空间的具体位置。在执行系统调用时,程序运维空间将会从用户空间转移到内核空间。处理完毕后再返回用户空间。

用户编程接口(API

用户编程接口(application program interface,API)是为了用户编程过程提供的各种功能库函数,如分配空间、拷贝字符、打开文件等。Linux用户编程接口(API)遵循了在Unix中最流行的应用编程解密标准-POSIX标准。它与系统调用之间存在一定的联系和区别。不同的语言和平台为用户提供了丰富的编程接口机,包括网络编程接口、图形编程接口、数据库编程接口等,但这些不是系统调用。

系统调用与用户编程接口它们之间存在联系,一个或者多个系统调用会对应到一个具体的应用程序使用的API;但是,并非所有的API都需要使用到系统调用。系统调用你可以看着一个函数定义,说明了如何获得一个个定的服务;而API是通过软中端向内核态发出一个明确的请求。如果按照层次关系来分,系统调用为底层,而用户编程接口更为上层。一个用户编程接口由0个或者多个系统调用组成。

一、系统调用和库函数的关系

系统调用通过软中断int 0x80从用户态进入内核态,函数库中的某些函数调用了系统调用。函数库中的函数可以没有调用系统调用,也可以调用多个系统调用。编程人员可以通过函数库调用系统调用。高级编程也可以直接采用int 0x80进入系统调用,而不必通过函数库作为中介。如果是在核心编程,也可以通过int 0x80进入系统调用,此时不能使用函数库。因为函数库中的函数是内核访问不到的。

二、从用户调用库函数到系统调用执行的流程

为了通过系统调用号来调用不同的内核服务例程,系统必须创建并管理好一张系统调用表。该表用于系统调用号与内核服务函数的映射,Linux用数组sys_call_table表示这个表。在这个表的每个表项中存放着对应内核服务例程的指针,而该表项的下标就是该内核服务例程的系统调用号。Linux规定,在I386体系中,处理器的寄存器eax用来传递系统调用号。

通常情况下,abc()库函数对应的服务例程的名字是sys_abc()。下图表示了系统调用和应用程序、对应的封装例程、系统调用处理程序及系统调用服务例程之间的关系。下面使用这个例子来简单说明系统调用过程。

Linux系统原理之系统调用

1)用户程序中调用库函数abc()。

2)系统加载libc库调用索引和参数后,执行int $0x80中断(0x80中断对应的中断例程被称为system call handler)或者sysenter汇编指令进入系统调用,因为中断使得进程从用户态进入内核态,所以参数通过寄存器传送。执行system_call函数。

3)System_call()函数根据传递过来的参数处理所有的系统调用。使用system_call_table[参数]执行系统调用。

4)系统调用返回。

5)执行iret或者sysexit汇编指令两种方式退出系统调用,并调用resume_userspace()函数进入用户空间。

6)继续在libc库中执行,执行完成后返回到用户应用程序中。

另外使用“strace”命令,后面直接跟Linux命令,可以直接查看整个ls操作的过程,调用了哪些库函数和系统调用。

系统调用中输入输出的参数为实际传递的值或者是用户态进程的地址,或者是指向用户态函数指针的数据结构地址。传递的参数放在寄存器eax中,即系统调用号。寄存器传递参数的个数满足两个条件。一是参数的长度不超过寄存器的长度,如果是32位平台不超过32位,64位平台不超过64位。二是不包括eax中的系统调用号,参数的个数不超过6个。

再谈系统调用和库函数

系统调用
系统调用提供的函数如open, close, read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如 fd=open(/”/dev/video/”, O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:
0-standard input,1-standard output,2-standard error。
系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。

系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数(系统调用越少系统开销越小性能越好)。这一结果又缘于缓冲区技术,在用户空间和内核空间,对文件操作都使用了缓冲区。例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。

库函数调用

标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose, fflush, fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_t fwrite(const void *buffer, size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen(/”~/proj/filename/”, /”w/”)。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。库函数调用通常用于应用程序中对一般文件的访问。库函数调用是系统无关的,因此可移植性好。由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (1)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!