Linux操作系统-系统级IO

Posted TangguTae

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统-系统级IO相关的知识,希望对你有一定的参考价值。

目录

基本概念

open函数

用法

文件描述符是什么?

read和write函数

read函数

write函数 

文件重定向

原理

追加重定向

dup2函数

文件系统

文件分区

inode

硬链接与软连接

总结 


基本概念

I/O:输入输出,是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作时从主存复制数据到I/O设备。

所有的I/O设备在Linux系统下被模型化为文件,所以可以这样说,Linux下一切皆文件。Linux内核引出一个简单、低级的应用接口(一系列的系统函数)来操作文件,称为Unix I/O。

在C语言中,C标准库中提供的像printf、scanf、fopen、fwrite等等I/O函数,在Linux系统中则是open、write、read等系统级I/O函数。高级别的I/O函数其实就是通过这些系统级的函数来实现的。

每个Linux文件都有自己的类型,例如普通文件(包含任意数据)、目录(包含一组链接的文件)、套接字(socket网络通信的文件)、管道(进程间通信)等等


open函数

用法

open函数将filename转换为一个文件描述符,如果打开不成功返回-1,打开成功返回文件描述符

open第一个参数是带路径或者不带路径的文件名称(默认当前路径)

第二个参数flags表示打开的方式

比如flags可以设置成O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)的方式打开,改参数可以设置多个模式,比如O_RDONLY | O_CREAT表示以只读的方式打开,如果没有该文件,就创建一个同名的文件。

最后一个参数是新建一个文件默认给它的权限是什么(注意,关于全限这里还需异或umask)。

例子:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
int main()

  int fd = open("log.txt",O_RDONLY);//以只读的方式打开log.txt
  if(fd < 0)//打开失败
  
    cerr<<"open error"<<endl;
    exit(1);
  
  char buf[128]='\\0';

  ssize_t s = read(fd,buf,127);//从打开的文件中读取内容
  if(s > 0)
    buf[s] = '\\0';                       
  cout<<buf<<endl;
  cout<<fd<<endl;
  //需要close文件描述符
  return 0;


先建好文件,将hello world重定向到log.txt文件当中,运行的结果如上图所示。

成功打开文件,文件描述符为3。

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
int main()

  int fd1 = open("log1.txt",O_RDONLY|O_CREAT);
  int fd2 = open("log2.txt",O_RDONLY|O_CREAT);
  int fd3 = open("log3.txt",O_RDONLY|O_CREAT);
  int fd4 = open("log4.txt",O_RDONLY);
  int fd5 = open("log5.txt",O_RDONLY);

  cout<<fd1<<endl;                               
  cout<<fd2<<endl;
  cout<<fd3<<endl;
  cout<<fd4<<endl;
  cout<<fd5<<endl;
  //需要close掉文件描述
  return 0;

期初没有log1-5这5个文件,前三个open打开时增加O_CREAT选项,默认没有则创建,后面则不然。

 

 打开成功返回文件描述符,负责返回-1,然后创建对应的文件。

文件描述符是什么?

这些数字本身是数组下标

上述例子发现新打开一个文件描述符默认是从3开始的,那么既然是数组下标,那么前面的0,1,2代表什么?

描述符0:标准输入(stdin)

描述符1:标准输出(stdout)

描述符2:标准错误(stderr)

在一个进程里面可能会打开多个文件,所以这些文件描述符需要被管理起来(先描述,后组织)

底层原理

1、每个文件对应一个结构体 struct file(和进程类似,task_struct)(描述)。

2、将文件连接起来(链表的方式),这样就可以通过一个文件找到其他文件的位置。

3、将进程的PCB与文件关联起来(1:n的关系),linux内核采用数组的方式关联。

这个就是文件描述符的数组fd_array[],struct file* fd_array[],用一个指针数组来存储文件对应的结构体指针。

在task_struct结构体里有一个struct files_struct*成员变量

在这个结构体里面又有fd_array这个struct file*的成员变量

 

再往里面走一走,看看这个指针数组指向的内容到底是什么:

在fs.h这个头文件里定义了struct file结构体,struct file这个结构体比较复杂,大概包含文件的属性信息、文件的缓冲、位置、inode信息和一些方法。

 struct file_operations 定义了很多方法

可以发现里面都是函数指针的形式,原因是对于不同的设备(硬件:硬盘、网卡、键盘等)他的读写的方法不一样,所以对应的操作系统打开这些设备,对这些设备操作的时候方法(读、写)也要有区别,所以用函数指针的方式指向他们各自的方法(驱动)。

所以整体的逻辑就是进程task_struct中有指向files_struct的结构体,files_struct中有指针数组fd_array存放file类型的指针,file类型的结构体中又有一个file_operations的成员变量,在file_operations结构体中最终的系统函数read、write等函数指针。


read和write函数

read函数

read函数从描述符fd的当前文件位置读取(复制)最多count个字节到内存的buf位置。 一般选择少读一个字符,然后人工加入'\\0',因为read是不关心'\\0'。

返回值是一个ssize_t类型,返回值为-1的时候表示读出错,如果返回值为大于0表示实际读到了多少字节,如果返回值等于0表示读到了文件的结尾。

write函数 

write函数是从内存位置buf复制至多count个字节到文件描述符所对应的文件 。

如果写入成功,返回写入的字节数,如果不成果返回-1;


文件重定向

对于linux下要把命令行的参数重定向到文件中可以用 > 或者 >> 来进行重定向

例子:

原理

对于进程来说,默认的文件描述从3开始,默认从最小的没有被分配的fd开始分配

默认情况下三个file分别为0,1,2,对应的0:硬件为键盘(stdin):1:显示器(stdout)、2:显示器(stderr)。

如果我们1给关掉,即把标准输出的文件描述符给close掉,close(1); 此时如果在打开一个文件,默认分给这个文件的文件描述符为1。

上述过程是在内核里完成的,应用层的命令行并不知道放fd = 1已经不是stdout了,所以这就叫重定向。

例子:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
int main()

  close(1);//关掉标准输入
  int fd = open("log.txt",O_WRONLY|O_CREAT);//新打开一个文件
  cout<<"hello world"<<endl;//输出
  cout<<fd<<endl;
  close(fd);                                 
  return 0;

追加重定向

O_APPEND

 打开文件时多加一个O_APPEND选项,就可以实现追加重定向

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
int main()

  close(1);//关掉标准输入
  int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0644);//新打开一个文件
  cout<<"hello world"<<endl;//输出
  cout<<fd<<endl;
  close(fd);                                 
  return 0;

运行两次test,发现连续往同一个文件里写入了两次相同的数据,实现了追加重定向。

小细节:

这些程序运行后的进程为什么打开了0,1,2这三个文件描述符,而不需要程序员进行open?

因为最开始的那个进程打开了输入输出,fd前三个元素在最开始的进程中已经指向了对应的struct file,所以在创建子进程时会按照父进程的模板创建,所以也同样的默认的打开了输入输出文件。注意:fd还是数据,一旦修改会发生写时拷贝

dup2函数

dup2函数复制描述符表项oldfd到newfd之中,覆盖newfd之前的内容。如果newfd已经打开了,dup2会在复制之前关闭newfd。注意,这个拷贝是对内容的拷贝而不是对数字的拷贝。

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
int main()

  int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0644);//新打开一个文件
  dup2(fd,1);//用dup2来重定向
  cout<<"hello world"<<endl;//输出
  cout<<fd<<endl;
  close(fd);                                 
  return 0;

注意:

文件描述符也是系统资源,打开了记得关闭,不然会造成系统泄漏。


文件系统

Linux文件系统(ext列),Windows文件系统(NTFS)

文件 = 属性 + 内容

通常情况下,文件会将属性与内容分开存放,文件的属性存放在inode里面,文件的内容存放在block里面。

文件分区

磁盘是一个非常大的存储介质,是典型的的块设备需要将物理内存映射到逻辑内存,然后将其管理起来。一个磁盘有多个分区,一个文件分区:

一个文件分区有多个Block group,每一个Block group都是相同的结构。

  1. 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  2. GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
  3. 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  4. inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  5. i 节点表(inode Table):存放文件属性 如 文件大小,所有者,最近修改时间等。
  6. 数据区(Data blocs):存放文件内容。

inode

inode的内容主要记录文件的属性以及该文件实际数据的位置

inode Table中可以存放多个inode,但是inode的个数有限制,所以会存在有空间而没办法新建文件的可能。

Data blocks存放数据,里面有很多block,一个block的大小是由格式化的时候确定的(可以设定1k,2k,4k字节)。

一个文件对应一个inode,而一个文件可以有多个block(当文件太大),所以他们之间的对应关系为 文件:inode:block =1:1:n。

所以说我们要找到一个对应的文件,需要先找到inode,而inode有一个id,在Linux下通过 ls -li 查看每个文件的所对应的inode的id。

最左边就是文件的inode对应的id,其实目录本身也是一个文件里面只存文件名和对应的inode的映射关系。 inode id是操作系统所关心的,而我们用户层面只关心的文件名等一些信息。

删除一个文件:

只需要将inode Bitmap中对应的位置0,对应的block Bitmap中的位也置为0。

其中的数据并没有被抹去,所以可以恢复数据

硬链接与软链接

硬链接:和指向的文件共享同一个inode,不是一个独立的文件。

ln 创建链接(默认硬链接)

可以看到具有相同的inode。这两个文件是共享的,删除其中一个不影响另外一个。

中间的一串数字代表硬链接文件的个数 ,包括自己。

一个文件目录默认是2,因为目录下面默认有两个隐藏文件

 

 一个是当前目录. 和上一级目录.. 。

软链接:具有独立的inode ,是一个独立的文件

ln -s可以软链接

 属性为l代表链接文件,他的inode与log.txt的inode不同,所以是一个独立的文件。

总结 

这些东西有点绕,需要总结一下

当我新建一个文件的时候,文件(struct file) = 属性 + 数据,属性对应着inode,每个inode对应一个id,linux内部通过inode id来识别文件,所以说系统并不记录文件名(文件名存放在目录当中,目录建立文件名与inode id的映射关系),只认识文件的inode id,而inode里面是包含很多属性信息的,还包含block在dataBlocks中的映射关系,这种映射可以找到数据在磁盘的哪个地方。当进程打开文件的时候,会返回一个文件标识符fd来记录此时打开的是哪一个文件,接下来的read,write等操作,是直接操作文件标识符fd来实现的,而管理文件标识符的是file* fd_array这个指针数组,fd就是指针数组的下标。fd_array又在files_struct这个结构体当中,在创建一个进程的时候,存在一个files_struct* 的指针指向这个结构体,最终由进程所管理。

以上是关于Linux操作系统-系统级IO的主要内容,如果未能解决你的问题,请参考以下文章

《信息安全系统设计基础》第7周学习总结

分离 CPU 和 IO 时间

Linux详解 --- 系统文件IO操作与文件描述符

:系统级I⁄O

IO系统

Linux 操作系统原理 — 内存 — 物理存储器与虚拟存储器