系统I/O与底层

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统I/O与底层相关的知识,希望对你有一定的参考价值。

文章目录

系统I/O

一、I/O的概念

输入/输出(I/O)是在内存和外设之间复制数据的过程。输入操作是从外设复制数据到内存,输出操作是从内存复制数据到外设。

所有语言提供的I/O库,如C语言的printfscanf,C++重载的>><<,都被称为标准I/O接口,它们都是在操作系统提供的系统级I/O接口的基础上实现的。

Linux下一切皆文件

Linux系统秉持着"一切皆文件"的原则,将所有的I/O设备,如磁盘、键盘、网络等,都模型化为统一的"文件"。

其最大的好处在于:允许用户使用同一套系统I/O接口对这些设备进行输入/输出操作。

二、系统级I/O接口

1、打开文件

int open(const char *pathname, int flags, .../*mode_t mode*/)

  • pathname:目标文件的绝对路径或相对当前工作目录的路径
  • flags:打开方式,包括:
  1. O_RDONLY:只读

  2. O_WRONLY:只写

  3. O_RDWR :可读可写

  4. O_APPEND:追加,必须和O_WRONLY搭配使用,即O_APPEND | O_WRONLY

  5. O_CREAT:如果目标文件不存在,则以pathname为路径创建该文件

注:flags可以是几个标志按位或,比如O_CREATE | O_WRONLY表示写文件,如果该文件不存在,则先创建。

  • mode:可变参数列表中的参数,如果需要创建文件,则需要传该参数(8进制)作为新文件的默认权限,最终权限由“默认权限 & ~umask”决定,其中umask为文件权限掩码
  • RetVal:返回打开文件的文件描述符,用户通过该文件描述符进行I/O操作

2、读文件

ssize_t read(int fd, void *buf, size_t count)

  • fd:目标文件描述符
  • buf:输出型参数,用作存储缓冲区
  • count:最多读取的字节数
  • RetVal:一个ssize_t类型的整数(本质为long类型)。如果成功,则返回本次实际读取到的字节数,0表示遇到EOF,失败则返回-1

3、写文件

ssize_t write(int fd, const void *buf, size_t count)

  • fd:目标文件描述符
  • buf:输入型参数,写入内容的存储缓冲区
  • count:要写入的字节数,一般为sizeof(buf)
  • RetVal:一个ssize_t类型的整数(本质为long类型),如果成功,则返回本次实际写入的字节数,失败则返回-1

4、关闭文件

int close(int fd)

将文件描述符为fd的文件关闭,成功则返回0,失败则返回-1。

注:所谓关闭,就是将当前占用fd的文件相关结构体释放,之后该fd还可被分配给新打开的文件。

三、文件描述符(file descriptor)

1、什么是文件描述符

文件描述符,简称fd,本质是一个非负整数,用户可以使用它来进行对应文件的I/O操作。

Linux内核为了记录每个进程打开的哪些文件,在进程控制块task_struct中保存了一个维护已打开文件的结构体files_struct,该结构体内部维护了一个文件记录表**fd_array**用来保存文件的具体信息。而所谓的fd,本质上就是这个文件记录表的数组下标,内核通过fd就可以索引到具体的文件。

2、文件操作方法

fd_array数组中存储了file结构体,该结构体包含了各种文件相关信息。其中,f_op是一个file_operations结构体,该结构体维护了操作该文件的各种函数指针,如readwrite等。

由此可以看出:

Linux下将各种I/O设备都模型化为文件系统的一个结构体,且根据不同类型的文件,操作系统会为它注册不同的操作方法。

3、文件描述符分配规则

Linux下每个进程都会默认打开三个“文件”:标准输入、标准输出和标准错误,它们的文件描述符分别为0、1、2。

之后再打开的文件,它的文件描述符是当前未被占用的最小的fd

C语言下的三大标准文件

这三个默认文件在C语言中分别对应FILE*类型的文件指针:stdin、stdout、stderr

C语言的FILE结构体原型是_IO_FILE

其中的_fileno保存文件描述符,其它的各类char*类型指针对应C语言维护的用户级缓冲区等信息。

四、用户缓冲区与内核缓冲区

用户级缓冲区由高级语言提供,是进程运行时用户空间的一段虚拟内存,如C语言的用户级缓冲区由FILE结构体维护。当用户向“文件”中读写时,优先使用这部分缓冲区的内容。

内核级缓冲区是由操作系统在内核空间为进程打开的文件所维护的,每个文件都拥有自己的缓冲区。

为了方便理解,以读写为例:

1、当用户使用标准I/O接口进行“读”操作时,首先操作系统会查看当前的用户缓冲区是否有可读数据,如果没有,则查看内核缓冲区是否有数据,如果还没有,则先将数据从外部I/O设备拷贝至内核缓冲区,再将内核缓冲区的数据拷贝至用户缓冲区;

2、当用户使用标准I/O接口进行“写”操作时,首先将数据拷贝至语言层提供的用户缓冲区。当满足语言规定的刷新策略时(比如行缓冲、全缓冲),操作系统会将用户缓冲区的数据拷贝至内核缓冲区,之后再根据内核提供的刷新策略(比如积累到一定量)将数据写入磁盘、网卡等外部I/O设备。

1、缓冲区刷新策略

  • 全缓冲

当读写的数据填满缓冲区时才进行对应的I/O操作,典型代表是对磁盘文件的读写。

  • 行缓冲

当输入和输出遇到换行符时才进行I/O操作,典型代表是键盘、显示器。

  • 不缓冲

不设立缓冲区,典型代表是stderr,方便错误信息快速地显示出来。

  • 其它刷新策略

进程结束后自动刷新、强制刷新(fflush()接口).

2、子进程与缓冲区

printf("printf()\\n");
fputs("fputs()\\n", stdout);

char* buffer = "write()\\n";
write(1, buffer, strlen(buffer));

fork();

直接在命令行运行该程序时,输出结果为:

把输出结果重定向至文件中时,文件内容为:

可以看到,文件中标准I/O接口printffputs分别输出了两次,而系统调用接口write仅输出一次,原因在于:

  1. 进程会创建父进程的虚拟地址空间副本,那么用户空间的用户缓冲区也理所应当地被子进程拿到。由于写时拷贝的存在,父子进程的用户缓冲区内容相同但相互独立,因此会被刷新两次;

  2. write属于系统调用接口,而系统调用接口会直接将数据拷贝至内核缓冲区,不存在用户级缓冲区

  3. 使用./test命令时,printffputs会向显示器文件写入数据。而显示器文件的刷新策略是行缓冲,因此在遇到’\\n’时,用户缓冲区的数据被立即刷新至内核缓冲区,并由内核决策何时将其写入显示器文件。

  4. 使用重定向>时,printffputs会向普通文件中写入数据。而普通文件的刷新策略是全缓冲,只有在用户缓冲区满时或进程结束才会将数据刷新至内核缓冲区,并由内核决策何时将将其写入普通文件。

3、缓冲区的意义

  • 对于内核缓冲区:

    1. 由于内核是各种硬件驱动的管理者,因此用户无法越过内核而直接读写外设数据。
    2. 外设的读写速度较慢,因此设立内核缓冲区,避免了与外设的频繁I/O,提高了系统的性能。
    3. 从缓冲区直接读数据提高了读操作的效率。
    4. 将数据直接写入缓冲区,至于什么时候刷新到磁盘由操作系统决定(延迟写),提高了写操作的效率。
  • 对于用户缓冲区:

    向内核缓冲区读写数据的标准I/O接口本质上使用了read()、write()这些系统调用接口。每当调用系统接口时,CPU都要从用户态切换至内核态,而这种切换会有时间消耗。当I/O较为频繁时,CPU状态切换的消耗就会相应叠加。因而设立用户缓冲区,减少因为系统调用而进行状态切换的次数。

4、标准I/O与系统I/O对比

  1. 标准I/O有用户级缓冲区,而系统I/O没有,因此标准I/O可以先将数据写入用户缓冲区,当缓冲区满再切换至内核态将其拷贝至内核缓冲区;而系统I/O每一次写都要切换至内核态,再向内核缓冲区写数据。因此标准I/O的效率更高
  2. 有些情况下必须使用系统I/O,比如向管道文件中读写、套接字编程。

五、重定向

为了方便用户将命令的输出结果保存到文件而不是直接在显示器上打印,bash提供了两个重定向操作符>>>

  • 命令 > 文件名 将对应文件内容清空,然后将命令的运行结果写入
  • 命令 >> 文件名在对应文件已有内容后追加

1、系统调用dup2()

int dup2(int oldfd, int newfd) // 失败返回-1

该函数本质上修改了当前进程控制块task_struct的文件记录表fd_array:先将原先newfd对应的文件结构体被删除,再将fd_array[oldfd]复制到fd_array[newfd],使得oldfdnewfd对应同一个文件

注:一个文件可以有不止一个fd与之对应

2、重定向操作符的实现原理

每个文件描述符都对应一个文件的描述信息用于操作文件。而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件

进程创建伊始会打开三个默认文件,其中fd=1的文件是显示器的标准输出文件

当调用dup2(fd, 1)时,标准输出文件的1号文件描述符对应的文件结构体被描述符为fd的新文件结构体覆盖,而printf这些I/O接口默认会将结果输出到1号文件,因此程序的运行结果就被写入到fd对应的文件了。

I.模拟实现

int fd = open("f.txt", O_CREAT | O_WRONLY, 0644);
dup2(fd, 1);
printf("dup2()\\n");

或者根据dup2()的原理,将代码改写成:

close(1);
// 注:将1号文件close后,新打开文件的fd自然就是1,那么printf就会将结果写入该文件了。 
int fd = open("f.txt", O_CREAT | O_WRONLY, 0644);
printf( "dup2()\\n");

II.运行结果

可以看到,原本应当输出到控制台的信息被重定向到了文件中。

以上是关于系统I/O与底层的主要内容,如果未能解决你的问题,请参考以下文章

android 底层入门开发

系统管理之小结

zz``文件系统磁盘布局与I/O映射

Linux系统I/O模型详解

带缓冲I/O 和不带缓冲I/O的区别与联系

Linux 标准 I/O 库