Linux入门基础IO

Posted 世_生

tags:

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

✔回顾C文件的接口

在学习C语言时我们了解了一些C语言的对于文件操作的接口。
其中有fopen、fclose、fputc、fgetc、fputs、fgets、fprintf、fscanf、fread、fwrite等

用一段代码简单回顾一下:

   #include<stdio.h>
   #include<string.h>
   int main()
   {
     	FILE* fp=fopen("myfile","a+");                                                               
     	if(!fp)
     	{
      		printf("fopen error\\n");
     	}
    	int count=5;
    	const char *m="hello linux\\n";
    	while(count--){
    	//对文件进行写入
    	fwrite(m,strlen(m),1,fp);
    }
    //关闭文件
    fclose(fp);
    return 0;
  }

我们先要了解,把内容写入文件中,先要有这个文件,然后就是要打开这个文件。
在上面的代码中

FILE* fp=fopen("myfile","a+");

第一个参数:文件的路径/文件名(不带路径会在当前路径下创建这个文件)。
当前路径:当前进程运行的路径。
第二个参数:就是以怎样的方式来。

在学习Linux时,我们经常听说“一切皆文件“。

那么显示器、键盘是文件吗?
在C语言时,我们经常用printf()函数来把内容显示到显示器上。
而现在,我们不用printf()函数来打印内容。

     #include<stdio.h>
     #include<string.h>
     int main()
     {
    	char *m="hello linux\\n";  
        fwrite(m,strlen(m),1,stdout);                                                                                             
       	return 0;
    }


这样我们可以了解,显示器也可以看作文件,也可以用fwrite()函数来写入。

重点来了:任何进程在运行时,都会默认打开三个输入输出流。
分别是:
标准输入(键盘)stdin
标准输出(显示器)stdin
标准错误(显示器)stderr
这三个流的类型都是FILE*,文件指针。


✔系统文件I/O

文件操作除了上述的C接口以外,还有我们的系统接口来进行对文件的操作。

我们用C接口操作文件在Linux上跑,其实是C在调用Linux的系统接口来完成的。
所以说,C库文件的接口是对系统调用接口的一次封装。

第一个接口:open接口,与C的区别是前面没有f。

   #include <sys/types.h>
   #include <sys/stat.h>
   #include <fcntl.h>

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

参数分别是:路径或文件名、选项、权限。

选项:

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开
    这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

其中注意的是这些接口的返回类型是int。
文件打开成功后,会返回一个较小的非负整数,表示该文件的文件描述符。
失败返回-1。

用一段代码来感受一下吧。

     #include<stdio.h>
     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<unistd.h>
     #include<string.h>
     int main()
     {
       umask(0);
      int fd=open("myfile",O_WRONLY|O_CREAT,0666);
      if(fd<0)
      {
        perror("open");
        return 1;
      }
      char *buf="hello linux\\n";                                                                
      write(fd,buf,strlen(buf));
    
      close(fd);
      return 0;
    }


其中一: open中的0666,表示创建文件的时候文件权限的666。当然这要设置一下默认掩码。
其中二:O_WRONLY|O_CREAT 表示如果有该文件就对该文件以只写的方式打开,如果没有就创建这个文件,权限为666,以只写的方式打开。

为什么要用O_WRONLY|O_CREAT来表示呢?
不难看出,这些用大写字母来表示的选项是用宏。这些宏都是对应一个bit位,像位图一样。
我们在传O_WRONLY|O_CREAT的时候,
会用if(O_WRONLY&F)来判断这个选项等等。

而在write中第一个参数是文件的描述符。而文件描述符又是什么呢?

✔文件描述符

我们先用一段代码来感受文件描述符:

   #include<sys/types.h>
   #include<sys/stat.h>
   #include<fcntl.h>
   #include<stdio.h> 
   int main()
   {
     umask(0);
    int fd1=open("myfile",O_WRONLY|O_CREAT,0666); 
    int fd2=open("myfile",O_WRONLY|O_CREAT,0666);
    int fd3=open("myfile",O_WRONLY|O_CREAT,0666);  
    int fd4=open("myfile",O_WRONLY|O_CREAT,0666);
  
    printf("fd1:%d\\n",fd1);
    printf("fd2:%d\\n",fd2);
    printf("fd3:%d\\n",fd3);
    printf("fd4:%d\\n",fd4);                                                                                        
    return 0;
  }


open执行成功返回一个较小的非负整形,也就是文件描述符。
通过上面的代码执行效果来看,有点像一个数组的下标。
其实这就是一个数组的下标,其中数组的0、1、2下标分被键盘(标准输入)、显示器(标准输出)、显示器(标准错误)给占了。
所以分配下来的是3、4、5、6。

而为什么是数组呢?
那么我们先要了解内存文件和磁盘文件了。

上面的代码创建了myfile文件,其文件的属性(文件大小、文件名、最近一次修改文件的时间等)会以struct file结构体在内存中保存起来。
而该文件的内容是在磁盘上的,也就是磁盘文件。
而这些struct file会被操作系统用双链表的形式来组织起来,和PCB类似。

而一个进程创建会创建PCB,其中PCB中有一个指针指向一个叫files_struct的结构体,其结构体中有一部分是以指针数组的形式存在的,其中存放的内容就是struct file结构体的地址。

进程通过文件描述符找到这个存放文件地址的地放,进而来对文件进行操作。

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。
于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进
程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的分配规则

如果我们close(0)

     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<stdio.h>  
     #include<unistd.h>
     int main()
     {
      close(0);
      umask(0);
      int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
    
      printf("fd1:%d\\n",fd1);
      return 0;
    }


文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

重定向

我们如果关闭close(1)

	 //输出重定向
     #include<sys/stat.h>
     #include<sys/types.h>
     #include<fcntl.h>
     #include<stdio.h>
     #include<unistd.h>
     #include<string.h>
     
     int main()
     {
      close(1);
      umask(0);
   	  int fd1=open("myfile",O_WRONLY|O_CREAT,0666);    
      char *duf="hello linux\\n";
	  
	  //printf("%s",duf);
	  //fflush(stdout);//更新流的用户空间缓冲数据
      write(1,duf,strlen(duf));                                                                 
      return 0;
     }

✔FILE

在学习C语言中的对文件操作的函数中有FILE*类型的。

FILE *fopen(const char *path, const char *mode);

那么FILE*是什么呢?

FILE是一个结构体,FILE*是一个结构体的指针。
我们都知道,C库中的IO相关的函数其实是对系统调用的封装。
在系统调用的IO型接口中,open函数的类型是int型。

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

open函数返回的是一个文件描述符。可以通过文件描述符来找到对应的文件。

而FILE结构体中就有一个int型的变量来表示这个文件描述符,这就是为什么C中的IO也可以找到对应的文件,这就是一种封装。

我们来看一下FILE结构体中的代码。

在/sur/include/stdio.h 可以找到
FILE中的int _fileno就是对文件描述符的封装。

struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
 char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;

 int _fileno; //封装的文件描述符

//……

我们来看下面一段代码:

#include<stdio.h>
#include<sys/stat.h>
#include<sys/tyoes.h>
#include<fcntl.h>
#include<string.h>

int main()
{
	close(1);
	int fd=open("myfile",O_WRONLY|O_CREAT,0666);
	
	const char* arr="hello linux\\n";
	
	fwrite("arr",strlen(arr),1,stdout);
	
	return 0;
}


字符串并没有打印到显示器上,而是被写入到了myfile文件中。

为什么呢?

在这之前先要了解,C中的stdin、stdout、stdree这三个流都是FILE*型的,并且这三这FILE中的文件描述符被固定为0、1、2。这就是为什么在C中用stdin、stdout、stderr就能找到键盘、显示器、显示器。

而在上面代码中关闭了1,myfile的文件描述符是1,所以在fwrite函数中用stdout还是写入到了myfile文件中。这就是为什么显示器上没有打印,而写入到了myfile文件的原因。而通过这段代码,我们现在应该要了解FILE*是什么了。

最后,fopen究竟做了什么?
1、给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)
2、在底层通过open打开文件,并返回fd,把fd填充进FILE变量的fileno中。

缓冲区

有两段代码:

#include<stdio.h>
#include<unistd.h>
void A()
{
	printf("hello linux\\n");
	sleep(3);
}

void B()
{
	printf("hello linux");
	sleep(3);
}

int main()
{
	A();
	B();
	return 0;
}

其中A是先显示hello Linux,再等待3秒。
B是先等待3秒,再显示hello Linux。

把内容回显给显示器时,内容先被写入缓冲区,采用的是行缓存,当遇到\\n时就会刷新缓冲区,把内容写人显示器中,当缓冲区内容被写满时也会刷新缓冲区。

缓冲区有:

  1. 无缓冲
  2. 行缓冲:遇到\\n就会把缓冲区\\n之前的内容刷新出来,否则等缓冲区写满。效率和和用性做的平衡。
  3. 全缓冲:等缓冲区写满

(常见对显示器内容刷新,用的是行缓存,这样才能更快的看到我们的内容)

看代码

#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main()
{
  printf("hello printf\\n");
  fprintf(stdout,"hello fprintf\\n"); 
  const char*mag2="hello write\\n";
  write(1,mag2,strlen(mag2));
  fork();
  return 0;
}

运行结果是:

hello printf
hello fprintf
hello write

但是我们对该进程进行输入重定向到文件中:./a.out > myfile

hello write
hello printf
hello fprintf
hello printf
hello fprintf


因为

当我们重定向后,文件描述符1已经不表示显示器了,而是我们的文件。这时候,缓冲区采用的是全缓存。
系统调用的IO接口是无缓冲,可以直接写入。
当缓冲区中存放了“hello printf\\n”和“hello fprintf\\n”时,创建了子进程,在return 0;之前进行了写时拷贝,所以最后打印了两次字符串。

这里面的缓冲区是C提供的,也是由FILE结构体进行维护。
缓冲区是在内存中的,在用户层。
缓冲区的数据刷新不是直接刷新到文件中,而是要经过内核区再写入到文件,这里有OS自己的刷新机制,这里不谈(我还没学到,哈哈)。

fclose和close

fclose:在关闭1之前,刷新了C中的缓冲区。内容可以被写入到文件内。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()
{
  close(1);
  int fd=open("myfile",O_WRONLY|O_CREAT,0666);
  char *arr="hello linux\\n";
  fprintf(stdout,arr);
  fclose(stdout);
  return 0;
}


close:由于采用了全缓,当close(1)时,系统调用的看不见C的缓冲区,没有刷新缓冲区就关掉了,故没有写入到文件中。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()
{
  close(1);
  int fd=open("myfile",O_WRONLY|O_CREAT,0666);
  char *arr="hello linux\\n";
  fprintf(stdout,arr);
  //fflush(stdout);//可以在调用close之前,先刷新缓冲区。
  fclose(stdout);
  return 0;
}


调用fclose是先调用ffiush,再调用close。

✔dup2系统调用

在上面的重定向中,我们要先close(1),再打开文件,这样好繁琐。我们有一个更简单的方法。

int dup2(int oldfd, int newfd);
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>

int main()
{
  int fd=open("myfile",O_WRONLY|O_CREAT,0666);  
  char *arr="hello linux\\n"; 
  close(1);
  dup2(fd,1);  
  printf("%s",arr);
  return 0;
}

✔理解文件系统

文件系统是Linux的一个重要部分,在Linux中玩了有一段时间,我一直有一个困惑,文件是怎么创建出来的?通过学习,慢慢的我自己有了一点了解。

文件=文件的属性+文件的内容,我们在查看文件大小时,显示的是文件内容的大小,其属性信息并没有算在里面,这说明了,文件的属性和文件的内容是分离存储的,在磁盘上。文件属性叫做元信息。

我们先简单了解磁盘:
磁盘有扇区、磁道、柱面、磁头……
文件的写入到磁盘中,会对磁盘寻址,其中会对柱头、磁道、扇面来寻找要写入的内容的地方。

假设磁盘的大小为500GB,对这么大的空间进行管理,系统采用了分区(就像中国也有省,市,县一样)

inode

inode是任何一个文件属性的集合,Linux中几乎每一个文件都有一个inode编号。

文件的元信息就是保存在inode中的,inode是一个结构体。

上图为磁盘文件的系统图(内核内存映射肯定有所不同),磁盘是一个典型的块设备,磁盘的分区被划分为一个个block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。如政府管理各区的例子
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的可以在了解一下
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
  • inode Table:存放文件属性 如 文件大小,所有者,最近修改时间等
  • 数据区(Data blocks):存放文件内容

数据区中是一个一个的块,每个块的大小是4KB,用来存放数据。(存在多级索引,我还没学,就不讲了)

inode结构体中有一个数组(int block[12])记录块(Data blockse)的位置。
一个普通文件的创建。

先要去inode位图中找到未被使用的inode,并申请下来把文件的属性记录其中,如果要对该文件写入内容,则系统会根据内容的大小去块位图中申请所需要的空闲块,并写入内容。内核在inode上的磁盘分布区记录了上述块列表。之后,内核会把该文件的inode编号和文件名添加到所在目录文件中。该文件的inode编号和该文件的文件名对应起来。

目录的创建

目录也是文件,也有自己的inode编号。目录在创建的过程中和上面普通文件的创建有点类似,不同的是,目录文件的内容是存放目录下的文件名和inode指针,使这些文件名和inode指针一一对应起来。

ls 命令:

ls -l 命令

这也可以看出,目录和文件之前的联系。

文件的删除

文件的删除并没有那么复杂,只要把对应inode的位图中的数据改掉(把1置成0),对应块位图数据也修改掉(把1置成0)。这也就是为什么删除的文件可以恢复过来的原因,只要把位图再置回来。

创建一个新文件主要有一下4个操作:

  1. 存储属性
    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  2. 存储数据
    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
  3. 记录分配情况
    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466 abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

硬链接

ln 文件名 要创建的文件名


这两个文件的inode号相同,说明myfile-s不是一个独立的文件,只是在目录的数据中添加了一个新文件名,该文件名对应的ionde和myfile相同。

硬连接数

硬连接数的数量是,有多少个文件对应的inode编号相同。
myfile文件和myfile-s文件的inode编号相同,所以硬连接数位2。

想要释放这个文件对应磁盘空间释放,要把硬连接数变成0。删除一个相同inode编号的文件,该硬连接数-1。

也就是说:

  • abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode
    263466 的硬连接数为2。

  • 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

软链接

ln -s 文件名 要创建的文件名


硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。

文件的三个时间

用stat 文件名可以查看

  • Access 最后访问时间
    Modify 文件内容最后修改时间
    Change 属性最后修改时间

以上是关于Linux入门基础IO的主要内容,如果未能解决你的问题,请参考以下文章

Linux基础入门--IO重定向及管道

Linux入门基础命令

Linux入门基础命令

Linux入门基础命令

Linux入门基础命令—cp

Python基础入门自学——22--异步IO