系统函数与库函数的区别

系统函数

就是系统调用,系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互,是操作系统留给应用程序的一个接口。用户进程需要发生系统调用时,内核将调用内核相关函数来实现(sys_read(),sys_write(),sys_fork())。用户程序不能直接调用这些函数,这些函数运行在内核态,CPU 通过软中断切换到内核态开始执行内核系统调用函数。

用户态–>系统调用–>内核态–>返回用户态

库函数

库函数可以理解为是对系统调用的一层封装。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更方便于程序猿编码。

库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。可以参考下图来理解库函数与系统调用的关系。

库函数和系统调用.png

例如该图,fputcC语言的库函数,在执行该函数时,会去执行系统调用函数write,进而切换到内核区,实现功能。

库函数 vs 系统调用

比较一下两者在实现相同任务下的时间。当使用C语言中的fgetcfputc 与 系统调用函数中的open
write函数,分别对同一个文件实行cp命令且每次只读取一个字符,每次只写一个字符。

常理分析:在同样的任务下,C语言中的库函数fputtc会先去找到write然后在进行写操作,而write省去了这一步,所以理论上write的速度更快。来实测一下。
首先分别创建好两个需要测试的demo

//read,write
#include<stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
    int fd1, fd2;
    fd1 = open(argv[1], O_RDONLY);
    fd2 = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0664);
    if(fd1 == -1){
        perror("file open error\n");
        exit(1);
    }   
    char buf[1];
    int len;
    while( (len = read(fd1, buf, 1)) != 0){ //设置每次只读一个字符
        if(len == -1){
            perror("read error\n");
            exit(1);
        }
        write(fd2, buf, len); //每次也就只写一个字符
    }   
    close(fd1);
    close(fd2);
    return 0;
}
//fgetc fputc
#include<stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
    FILE *fp_in,*fp_out;
    fp_in = fopen("dict.txt","r");
    if(fp_in == NULL){
        perror("fpoen error\n");
        exit(1);
    }   
    fp_out = fopen("dict.cp","w");
    if(fp_out == NULL){
        perror("fpoen error\n");
        exit(1);
    }   
    int n;
    while( (n = fgetc(fp_in)) != EOF){
        fputc(1, fp_out);//一次只读只写一个字符
    }   
    fclose(fp_in);
    fclose(fp_out);
    return 0;
}

依照测试来看,一个66M的文件在C语言库函数的代码下瞬间完成,而在系统调用函数中却有明显是停顿。
image.png

原理

通过strace命令,它可以从头到尾跟踪一个可执行文件的执行,然后以一行文本输出系统调用的名字,参数和返回值。
分别执行两个程序可以得到如下结果

  • C语言库函数
    TIM图片20200518111825

  • 系统调用函数
    image.png

通过上图可以看出,在执行C语言系统函数时,并没有按照我们要求的每次执行只读一个与只写一个字符,而是每次读4096,每次写4096,但是readwrite是真实的按照我们设想的去一次一个字符进行操作,此时也就解释了为什么执行上述操作时,fgetc可以在一瞬间完成,而read却需要一些时间。

原理

预读入缓输出机制.png

图列介绍:

  • 最粗的黑线代表由内核空间到用户空间
  • kernel:内核区
    • 里面的方框代表内核的缓冲区
  • buf代表用户自动自定义的保存数据地方(相当于一个缓冲区?)
  • 蓝色方框代表:用户缓冲区

在执行写操作时,系统会将用户区要写入的东西优先存放的内核缓冲区,缓冲区满后再有系统内部的调度算法,一次性冲刷进磁盘。

在执行readwrite函数时,由于没有用户缓冲区,且自定义的buf被设置为$1$字节,那么每次进行一个字符的操作都会由用户区切换到内核区进行操作,而该操作是十分耗时的。

反观在执行fputc时,标准库函数会默认自带一个用户缓冲区,当使用库函数进行写操作时,会优先写入用户缓冲区(蓝色方框),然后等到用户缓冲区满,写入内核缓冲区。

fputcwrite在该情况下少了4096倍由用户区到内核区的切换,故速度更快。

预读入缓输出

我们认为用户程序用户定义buf直接使用系统调用write函数 跳过了标准库函数进入kernal
kernal写到磁盘文件上了实际上不是这样的。在内核中默认也维护了一个缓冲区 默认是4096b
内核为了提高效率会一次性等缓冲区满以后再刷到磁盘上,当自己写的write函数写的时候 实际上没有写到磁盘上,而是在内核的缓冲区,这种机制称为缓输出

预读入同理,磁盘会先将你要读入的东西放入内核缓冲区,而非调用一次read,在磁盘中读取一次内容。

正确理解库函数高效于系统调用

首先解释,上述说明的库函数性能远高于系统调用的前提是,库函数种没有使用系统调用。再来解释下某些包含系统调用的库函数,然而其性能确实也要高于系统调用。比如上篇文章中关于文件 IO 函数 freadfwritefputcfget 等,这些函数通常情况下性能确实比系统调用高,原因在于这些库函数使用了缓冲区,减少了系统调用的次数,因而显得性能比较高。
参考了《C 专家编程》书籍中的附录 A.4,书中关于两者区别的回答是这样的,函数库调用是语言或应用程序的一部分,而系统调用是操作系统的一部分。

  • 所有 C 函数库是相同的,而各个操作系统的系统调用是不同的。
  • 函数库调用是调用函数库中的一个程序,而系统调用是调用系统内核的服务。
  • 函数库调用是与用户程序相联系,而系统调用是操作系统的一个进入点
  • 函数库调用是在用户地址空间执行,而系统调用是在内核地址空间执行
  • 函数库调用的运行时间属于「用户」时间,而系统调用的运行时间属于「系统」时间
  • 函数库调用属于过程调用,开销较小,而系统调用需要切换到内核上下文环境然后切换回来,开销较大
  • 在C函数库libc中大约 300 个程序,在 UNIX 中大约有 90 个系统调用函数库典型的 C 函数:system, fprintf, malloc,而典型的系统调用:chdir, fork, write, brk

据书中记载,库函数调用大概花费时间为半微妙,而系统调用所需要的时间大约是库函数调用的 70 倍(35微秒),因为系统调用会有内核上下文切换的开销。纯粹从性能上考虑,你应该尽可能地减少系统调用的数量,但是,你必须记住许多 C 函数库中的程序通过系统调用来实现功能。