系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如 CPU、磁盘、打印机等)进行交互,是操作系统留给应用程序的一个接口。下面适用于访问设备驱动程序的系统调用:
系统调用 | 描述 |
---|---|
open | 打开文件或设备 |
read | 从打开的文件或设备中读取数据 |
write | 向打开的文件或设备中写入数据 |
close | 关闭文件或设备 |
ioctl | 把控制信息传递给设备驱动文件 |
用户进程需要发生系统调用时,内核将调用内核相关函数来实现(如 sys_read(), sys_write(), sys_fork())。用户程序不能直接调用这些函数,这些函数运行在内核态,CPU 通过软中断切换到内核态开始执行内核系统调用函数。
用户态–>系统调用–>内核态–>返回用户态
实际上使用系统调用会影响系统的性能,在执行调用时的从用户态切换到内核态,再返回用户态会有系统开销。为了减少开销,因此需要减少系统调用的次数,并且让每次系统调用尽可能的完成多的任务。硬件也会限制对底层系统调用一次所能写的数据块的大小。为了给设备和文件提供更高层的接口,Linux 系统提供了一系列的标准函数库。使用标准库函数,可以高效的写任意长度的数据块,库函数在数据满足数据块长度要求时安排执行底层系统调用。
一般地,操作系统为了考虑实现的难度和管理的方便,它只提供一少部分的系统调用,这些系统调用一般都是由 C 语言 和汇编混合编写实现的,其接口用 C 来定义,而具体的实现则是汇编,这样的好处就是执行效率高,而且,极大的方便了上层调用。
库函数(Library function)是把函数放到库里,供别人使用的一种方式。.方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。一般放在.lib文件中。库函数调用则是面向应用开发的,库函数可分为两类,一类是 C 语言标准规定的库函数,一类是编译器特定的库函数。(由于版权原因,库函数的源代码一般是不可见的,但在头文件中你可以看到它对外的接口)。
glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。这些基本函数都是被标准化了的,而且这些函数通常都是用汇编直接实现的。glibc 为程序员提供丰富的 API(Application Programming Interface),我们经常说到的POSIX(Portable Operating System Interface of Unix)是针对API的标准,即针对API的函数名,返回值,参数类型等。POSIX兼容也就指定这些接口函数兼容,但是并不管API具体如何实现。
随着系统提供的这些库函数把系统调用进行封装或者组合,可以实现更多的功能,这样的库函数能够实现一些对内核来说比较复杂的操作。比如,read() 函数根据参数,直接就能读文件,而背后隐藏的比如文件在硬盘的哪个磁道,哪个扇区,加载到内存的哪个位置等等这些操作,程序员是不必关心的,这些操作里面自然也包含了系统调用。而对于第三方的库,它其实和系统库一样,只是它直接利用系统调用的可能性要小一些,而是利用系统提供的 API 接口来实现功能(API的接口是开放的)。部分 Libc 库中的函数的功能的实现还是借助了系统掉调用,比如 printf 的实现最终还是调用了 write 这样的系统调用;而另一些则不会使用系统调用,比如 strlen, strcat, memcpy 等。
系统调用是为了方便使用操作系统的接口,而库函数则是为了人们编程的方便。
库函数调用与系统无关,不同的系统,调用库函数,库函数会调用不同的底层函数实现,因此可移植性好。由于库函数是基于 c 库的,因此不能用于内核对于底层驱动设备的操作。
hello world
程序是将信息打印到终端,终端对系统来说是硬件资源,如果没有系统调用,用户程序需要自己编写终端设备的驱动,以及控制终端如何显示的代码。总而言之,我们只需要把系统调用当作一个接口,而这个接口能实现我们的一个功能,既方便又安全。
函数库调用是语言或应用程序的一部分,而系统调用是操作系统的一部分。
库函数调用大概花费时间为半微妙,而系统调用所需要的时间大约是库函数调用的 70 倍(35 微秒),因为系统调用会有内核上下文切换的开销。纯粹从性能上考虑,你应该尽可能地减少系统调用的数量,但是,你必须记住许多 C 函数库中的程序通过系统调用来实现功能。
上述内容基本说清楚了库函数与系统调用的概念以及它们之间的关系,下面我们来理解系统调用到底是如何运行的。
当一个进程正在运行,遇到读写文件操作,会发生一个中断,中断后系统会把当前用户进程的一些寄存器信息保存在内核堆栈中,接着去处理中断服务程序,这里是要去执行系统调用,Linux 中通过执行 int $0x80
来执行系统调用的中断,但内核实现了很多系统调用,这时需要传递「系统调用号」来指明需要哪个系统调用。
为了更清楚的说明系统调用的过程,我们这里参考网上的一段代码来实现系统调用:
int main()
{
time_t tt;
struct tm *t;
asm volatile (
"mov $0,%%ebx\n\t"
"mov $0xd,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
: "=m" (tt)
);
t = localtime(&tt);
printf("Time: %d-%02d-%02d %02d:%02d:%02d\n",
t->tm_year + 1900,
t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
}
[linuxblogs@host ~]$ gcc a.c -oa && ./a
Time: 2018-05-06 03:23:46
首先通过 mov $0xd %%eax
来将系统调用放入 %eax
寄存器中,time() 的系统调用号是 13,然后执行 int $0x80
系统就会去执行 time() 这个系统调用了。
其实代码中的汇编部分就是实现 time() 系统调用的功能,汇编代码不懂没关系,这里主要是为了说清楚系统调用的整个过程。