内核态与用户态、系统调用与库函数、文件IO与标准IO、缓冲区等概念介绍

概述

Linux提供了两套可以用于文件的IO接口:

  • 文件IO: open、create、close、lseek、read、write、fcntl、ioctl等
  • 标准IO: FILE、fopen、fwrite、fread、等

为了理解文件IO和标准IO的区别,可能要先理解下用户态与内核态,系统调用与库函数的概念。

用户态和内核态

什么是用户态和内核态:

  • 内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
  • 用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取

区分用户态和内核态的作用:

之所以区分用户态和内核态,是为了限制不同程序的访问能力,防止它们获取随意获取其他程序或外围设备的数据。

(限制用户态的访问能力,还可以防止外围设备的访问冲突。)

我们知道,线程

用户态和内核态的切换:

程序都是工作在用户态,但是有时候程序需要访问一些受限的数据(如从硬盘读数据,从键盘获取输入等),这是就需要切换到内核态。切换的方式一般时调用系统调用。用户态通过系统调用通知内核态,需要访问哪些受限的数据,或操作哪些受限的设备。内核态就帮助用户态完成。

小结:

简单的说,用户程序只能访问内存,而操作系统能访问所有数据。用户程序工作的模式即是用户态,当用户程序想要访问受限的数据时,就需要向操作系统发请求,让操作系统帮忙完成。 操作系统工作的模式即内核态。

库函数和系统调用

库函数工作在用户态,系统调用工作在内核态。

所有的操作系统都会提供一些服务用以访问/操作设备等,操作系统会为这些服务提供接口。这些接口就是系统调用 。用户态程序通过调用系统调用可以切换到内核态,访问内存意外的数据,操作外围设备等。

不同的操作系统提供的系统调用会有所差异。

Unix为每个系统调用在标准C库中设置一个具有同样名字的函数 。 一般我们是称这些函数为系统调用。(如标准C库中有函数write(), write()函数直接使用write系统调用相应的内核服务)。 所以一般说write(),指的不是write()这个函数,而是系统调用write。

标准C库,或是其他库会定义一些函数,这些函数称之为库函数。很多库函数是跨平台的。虽然有些库函数最终会调用系统调用,但不少库函数会根据具体的操作系统选用响应的系统调用接口。

有些库函数仅仅是操作内存,不访问硬盘等外围设备的数据,因此最终并不会调用系统带哦用。即:并不是所有的库函数最终都要使用系统调用的, 如:strcpy、atoi等。

I/O的概念

I/O:输入输出。

既然是输入输出,就要有输入输出设备(一般内存不算输入输出设备)。如硬盘、键盘、终端等都可以作为输入输出设备。

用户态是不能访问硬盘、键盘、终端这些外围(虚拟)设备的,因此需要切换到内核态。用户态和内核态的切换是会产生开销的。

举个例子,程序要从硬盘上的文件读取数据,其过程如下:

  1. 程序通过系统调用(如read)告诉操作系统想要读取某个硬盘文件的内容。
  2. 切换到内核态(这里需要从用户态向内核态传递,要读取的是哪个文件等信息)
  3. 系统调用(工作在内核态)读取文件内容。
  4. 切换到用户态(这个过程包含了把系统调用读取到的数据拷贝到用户态的过程)
  5. 程序得到数据。

文件I/O——不带缓冲的I/O

文件IO相关的函数open、close、read、write等等

这里我们主要以write()函数为例,介绍一些文件IO的特性。

文件IO是不带缓冲的,所谓不带缓冲,即每调用一次文件IO(如write),就进行一次用户态与内核态的切换。

调用一次write,就需要立即把数据从用户态拷贝到内核态。

标准I/O——带缓冲的I/O

标准IO相关的函数主要有:fopen、fclose、fread、fwrite、fgetc等等

标准IO是带缓冲的。

标准IO提供缓冲的目的是尽可能减少使用read和write调用的次数(也就是用户态和内核态切换的次数)。

所谓带缓冲,其实就是在调用fwrite的时候,先把数据放在缓冲区。不直接使用系统调用,切换到内核态。而是等到缓冲区满或是调用fflush等条件满足时,再一次性调用write,把数据拷贝到内核空间。

缓冲

缓冲是标准IO库提供的,缓冲区存在于用户空间(不管是fread还是fwrite)。 对于fwrite,根据上面的例子很好理解。对于fread而言,就是在调用fread的时候,切换到内核态,内核态读取缓冲去能够接受的大小的数据(一般会多余此次fread期望的数据), 然后返回给fread函数。下次再调用fread的时候,可能只需要从缓冲区读取,而不需要再到内核空间拷贝了。

标准IO有三种缓冲类型:

  • 全缓冲:

  • 行缓冲

  • 不带缓冲

文件IO与标准IO的一些对比

效率对比

文件IO不带缓冲,因此每调用write,就需要做用户态和内核态的切换,把数据从用户态切换到内核态。

标准IO带缓冲,调用fwrite的时候,先把数据保存在缓冲区,等待缓冲区满或调用fflush等条件满足时,再调用write,一次性把多次fwrite的数据

因此,从效率上看,带缓冲的IO作用户态与内核态切换的次数较少,效率比较高。没有特殊要求的话,一般应该选用fopen、fwrite系列带缓充的IO。

文件IO与标准IO的应用场景——多线程日志系统为例

在多线程日志系统中,每个线程打开一个日志文件的描述符。

如果使用带缓冲的IO,最终可能导致日志的顺序发生错乱,可能影响阅读。因此如果对多线程的日志顺序有要求的话,可能需要使用不带缓冲的IO。

另一方面,在测试中发现,使用标准IO时,每次调用write的数据并不一定都是多次完整的fwrite的数据。可能会有一些fwrite的数据被分为两次write。举个例子,每次调用fwrite写100个字节的数据,而缓冲区为550个字节,那么第6次fwrite的数据可能就被截断分成两次write。(这是我在centOS7.2上的测试结论,目前不清楚是否可以配置在fwrite写缓冲之前先判断缓冲区剩余空间是否充足,不足时先调用write,再把完整的fwrite数据写入缓冲)。(就刚刚那个例子来说,就是不知道是否可以配置,在第6次fwrite写缓冲之前先write前面的500字节)。

你可能感兴趣的:(Unix环境高级编程)