基础IO介绍

目录

一.C语言文件io介绍

1.1函数介绍

 1.2stdin-stdout-stderr

二.系统文件IO

2.1理论

2.2系统调用接口的介绍

2.2.1open

2.2.2文件描述符fd

2.2.3write

 2.2.4close

2.2.5read

 2.3.5总结

3.重定向

3.1概念

 3.2dup2系统调用

 4.缓冲区

4.1FILE的理解

五.文件系统

5.1磁盘概念

5.2inode

5.3磁盘的分区介绍

六.软硬链接

6.1软连接

6.2硬链接

6.3区别


一.C语言文件io介绍

1.1函数介绍

从语言中的一些文件操作函数:

文件操作函数    功能
fopen    打开文件
fclose    关闭文件
fputc    写入一个字符

fgetc    读取一个字符
fputs    写入一个字符串
fgets    读取一个字符串
fprintf    格式化写入数据

fcanf    格式化读取数据
fwrite    向二进制文件写入数据
fread    从二进制文件读取数据
fseek    设置文件指针的位置
ftell    计算当前文件指针相对于起始位置的偏移量
rewind    设置文件指针到文件的起始位置
ferror    判断文件操作过程中是否发生错误
feof    判断文件指针是否读取到文件末尾

可在Linux操作系统下对几个函数进行演示:

int main()
{

  FILE* pf=fopen("data.c","w");
  if(pf==NULL)
  {
    perror("fopen");
  }
  fputc('A',pf);
  fputs("hello world\n",pf);
  char arr[5]="LLLL";
  fprintf(pf,"%s",arr);
  return 0;
}

结果:

 1.2stdin-stdout-stderr

一般来讲,c语言程序运行起来会默认打开这三个流。

可用man指令查看详细信息:

分别对应标准输入,标准输出,标准错误。

这些流也是FILE*类型的,程序运行起来时会自动打开, 我们不需要自己手动打开这些流,

例如:printf,gets等这些函数,里面的参数可以直接传stdin,stdout,可把它们看作是一个文件指针。(后面进行详细解释)

二.系统文件IO

2.1理论

当我们对某一个文件进行写入时,本质上是对磁盘进行写入数据。而这些操作是需要操作系统的参与的,但问题是操作系统是如何被上层使用的呢?例如:c语言中printf函数在不同的系统下都可以使用,是如何做到的呢?

答案:c语言中的prinf函数对操作系统提供的接口进行了封装。

why?

1.直接使用原生系统接口,使用成本比较高!
2.语言不具备跨平台性!
封装是如何解决跨平台问题的呢?——穷举所有的底层接口+条件编译!

2.2系统调用接口的介绍

2.2.1open

在Linux中,打开文件的接口是open

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

int open(const char *pathname, int flags, mode_t mode);   也可打开不存在的文件(通过mode来指明权限)。

参数解释:O_....这些是宏,一般是一个比特位是1,不能与其他宏重叠,可以进行或等运算等得出一个值。

pathname:要打开或创建的目标文件的路径名
flags:打开文件时,可以传入多个参数,用传入的参数进行或运算,得出flags
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读,写打开--这三个常量,必须指定一个且只能指定一个
O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限。
O_APPEND:追加写

mode:文件权限。在新文件被创建时,参数mode具体指明了使用权限。但通常也会被umask修改。所以一般新建文件的权限为(mode&~umask).

返回值:创建成功返回新的文件描述符fd;错误返回-1。

例如:

int main()
{
	int fd1 = open("log.txt1", O_RDONLY|O_CREAT, 0664);
	int fd2 = open("log.txt2", O_RDONLY|O_CREAT, 0664);
	int fd3 = open("log.txt3", O_RDONLY|O_CREAT, 0664);
	int fd4 = open("log.txt4", O_RDONLY|O_CREAT, 0664);
	int fd5 = open("log.txt5", O_RDONLY|O_CREAT, 0664);
	
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
	
	return 0;
}

结果:

 可以发现是有顺序的,其实是存在0 ,1,2的, Linux下,进程会默认把3个文件描述符分配(0,1和2)给标准输入,标准输出和标准错误,所以,后序如果打开文件,文件描述符就是从3开始分配的,并且这些数字是数组的下标。

2.2.2文件描述符fd

一个进程是可以管理多个文件的(当文件加载到内存时),当一个文件被打开时,也就调入到内存中,操作系统会为每个已经打开的文件(包括硬件)创建各自的struct file结构体,然后将这些结构体组织起来。为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。每一个进程的task_struct中都有一个File struct*的指针,它指向了一个指针数组。而每个文件对应的struct file结构体的地址便存入Files struct中。所以,只要知道Files struct 中的下标(文件描述符),就可以找到对应的文件信息,从而对文件进行操作。

注意:进程创建时会默认打开 标准输入,输出,错误流。这些硬件被操作系统识别到,与其它文件一样,会创建各自的struct file,把地址存入对应进程File struct*file指向的数组中,为它们分配下标0,1,2。

如图:

这样便于操作系统以统一的视角看待所有硬件设备,更好的管理。

fd的分配规则是从最小且为被分配的位置开始。

2.2.3write

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

 函数参数:

  • fd:在文件描述符为fd的文件中进行写入
  • buf:从buf位置开始读取数据
  • count:从buf位置开始读取count个字节到文件中

函数返回值:

  • 成功:返回实际写入数据的字节数
  • 失败:返回-1

例如:

int main()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0664);
  char arr[20]="hello world\n";
  write(fd,arr,11);
  return 0;
}

结果:

 2.2.4close

int close(int fd);

使用close函数传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

2.2.5read

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

 函数参数:

  •  fd:在文件描述符为fd的文件中开始读
  • buf:把读得内容从buf的位置开始存放
  • count:从buf位置开始存放count个字节

 函数返回值:

  • 成功:返回实际读取数据的字节数
  • 失败;返回-1

例如:


int main()
{
  int fd=open("log.txt",O_RDONLY);//以只读的方式打开
  if(fd<0)
  {
    perror("open");
  }
  else
  {
    char ch;
    while(1)
    {
      ssize_t k = read(fd, &ch, 1);
		if (k <= 0){
		  	break;
		}
	  	write(1, &ch, 1);//1对应的是标准输出流
    }
  }
  close(fd);
  return 0;
}

结果:

 2.3.5总结

c语言其实就是对这些系统提供的函数进行了封装。比如说上层的c语言函数接口 fopen/ fclose/ fread/ fwrite... 底层封装的是系统接口open/close/ read/write;在c语言类型对应的结构体FILE底层封装的是系统文件描述符fd;

3.重定向

3.1概念

是只修改原来默认的一些信息,对操作系统命令的默认执行方式进行了改变。

重定向的方式一般有:

输入重定向:

输出重定向:

追加重定向:

 例如:

输入重定向:

int main()
{
  close(1);//关闭标准输出流
  int fd=open("tt.t",O_WRONLY|O_CREAT,0666);
  if(fd<0)
  {
    perror("open");
    return 1;
  }
  printf("how are you\n");
  printf("hello world\n");
  fflush(stdout);
  close(fd);//此时的fd==1
  return 0;
}

结果:输出信息并没有输出到显示器出,而是输出到tt.t文件中了。

注意:c语言的printf等函数会自带缓冲区,如果是对于显示器,是行缓冲,若对于文件,是全缓冲。所以上面输出重定向后,是全缓冲,要在close之前将信息输出来。

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1 下标所表示内容,已经变成了文件tt.t的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写 入,进而完成输出重定向

追加重定向

int main()
{
  close(1);//关闭标准输出流
  int fd=open("tt.t",O_WRONLY|O_CREAT|O_APPEND,0666);
  if(fd<0)
  {
    perror("open");
    return 1;
  }
  printf("how are you\n");
  printf("hello world\n");
  fputs("hello you\n",stdout);
  fflush(stdout);
  close(fd);
  return 0;
}

结果:

输出重定向:可以把从"键盘文件"中读取的一些函数该文从tt.t文件中读取。只需把文件描述符0关闭掉,这样打开tt.t文件,分配给tt.t的文件描述符就是0.

例如:

int main()
{
  close(0);
  int fd=open("tt.t",O_RDONLY|O_CREAT,0666);
  if(fd<0)
  {
    perror("open");
    return -1;
  }
  char arr[100];
  while(scanf("%s",arr)!=EOF)
     printf("%s\n",arr);
  return 0;
}

结果:

 3.2dup2系统调用

int dup2(int oldfd,int newfd);

概念:复制文件描述符给一个新的文件描述符,让fd_array数组中下标为oldfd的内容拷贝给下标为newfd的内容,也就是让newfd的指向发生改变,指向oldfd所指向的文件。

函数返回值:成功返回newfd,失败返回-1;

举个栗子:

int main()
{
  int fd=open("text.c",O_WRONLY|O_CREAT,0666);
  if(fd<0)
  {
    perror("open");
    return -1;
  }
  dup2(fd,1);
  printf("hello world\n");
  fputs("how are you\n",stdout);
  char arr[100]="i am fine\n";
  char arr2[100]="thanks\n";
  write(fd,arr,12);//fd,1都是指向text.c文件
  write(1,arr2,12);

  return 0;
}

 结果:与上面的输出重定向的原理是差不多的

画图理解下: 

 假如把文件描述符1先关闭,则不能把数据写到text.c文件中。

int main()
{
  int fd=open("text.c",O_WRONLY|O_CREAT,0666);
  if(fd<0)
  {
    perror("open");
    return -1;
  }
  dup2(fd,1);
  close(1);
  printf("hello world\n");
  fputs("how are you\n",stdout);
  return 0;
}

结果: 

 4.缓冲区

4.1FILE的理解

FILE是C语言的一个对文件进行描述的一个结构体。因为IO相关的函数与系统调用接口是相对应的,且库函数封装了系统调用,所以本质上访问文件都是通过fd进行访问的,在c语言中的FILE结构体中肯定是封装了fd。

看查看相应的源码:


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; //封装的文件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
 
#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];
 
	/*  char* _save_gptr;  char* _save_egptr; */

	_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

FILE结构体里面的——fileno就是封装的fd,里面还可以看到缓冲区相关的内容。这里的缓冲区是指c语言里面的缓冲区。

缓冲区的刷新策略:不缓冲,行缓冲,全缓冲。

看下面的例子:

int main()
{

	printf("hello printf\n");
	fputs("hello fputs\n", stdout);
	write(1, "hello write\n", 12);
	return 0;
}

 结果:

int main()
{
	
  int fd=open("text.c",O_WRONLY|O_CREAT,0666);
  if(fd<0)
  {
    perror("open");
    return -1;
  }
    dup2(fd,1);
	printf("hello printf\n");
	fputs("hello fputs\n", stdout);

	write(1, "hello write\n", 12);
	fork();
	return 0;
}

结果:

会发现c语言中的打印函数输出了两边,而系统调用的write只输出一遍。

why?

因为系统调用的write是直接写到OS里面去了,可看作没有缓冲区。但在dup2(fd,1)后,c语言中的那些函数是输出到text.c中,变成了全缓冲,只是写到了缓冲区里面去了,要在结束后才会刷新到OS中。而又进行了fork调用,两个进程共享这些内容,但刷新时会发生写时拷贝,缓冲区的内容也就变成了两份,所以输出了两份相同的内容。

补充:操作系统其实也是有缓冲区的。当刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或显示器上。  

五.文件系统

文件分为磁盘文件和内存文件,下面介绍了磁盘文件。

5.1磁盘概念

磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备,与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。

磁盘在冯诺依曼体系结构中既可以充当输入设备,又可以充当输出设备

对磁盘进行读写操作时,一般有以下几个步骤:

1.确定读写信息在磁盘的哪个盘面

2.确定读写信息在磁盘的哪个柱面

3.确定读写信息在磁盘的哪个扇区

通过以上步骤,可以确定信息在磁盘中的读写位置。

5.2inode

概念:inode是在Linux操作系统中的一种数据结构,其本质是结构体,它包含了与文件系统中各个文件相关的一些重要信息。在Linux中创建文件系统时,同时会创建大量的inode。

磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名,文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。

 在命令行输入ls -li 可查看inode

 无论是文件内容还是文件属性,都是存储在磁盘当中的。

5.3磁盘的分区介绍

线性存储介质

理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。

磁盘分区

磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可以被分为十亿多个扇区。

计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分, 盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件性质分的越细,按照更为细节的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。

在Linux操作系统下,我们也可以通过以下命令查看我们磁盘的分区信息。

磁盘格式化 

当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。

其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2,EXT3,XFS等。
 

EXT2文件系统的存储方案 

计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group).

注意:启动块的大小是确定的,而块组的大小是由格式化确实的,并且不能更改。

其次,每个组块都有着相同的组成结构,每个组块都由超级快(Super Block),快组描述符表(Group Descriptor Table),块位图(Block Bitmap),inode位图(inode Bitmap),inode表(inode Table)以及数据表(Data Block)组成。

inode Table: 存放文件属性,即每个文件的inode,里面有一个inode编号。
Data Blocks:以块为单位,4KB, 存放文件内容。

Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。
Block Bitmap: 块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
inode Bitmap: inode位图当中记录着每个inode是否空闲可用。

 Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。


注意:

  1. 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
  2. 磁盘分区并格式化后,每个分区的inode个数就确定了。

如何理解创建一个空文件?

通过遍历inode位图的方式,找到一个空闲的inode
在inode表中找到对应的inode,并将文件的属性信息填充进inode结构中
将该文件的文件名和inode指针添加到目录文件的数据块中。


如何理解对文件写入信息?

通过文件的inode编号找到对应的inode结构。
通过inode结构找到存储该文件内容的数据块,并将数据写入数据块
若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。


如何理解删除一个文件?

将该文件对应的inode在inode位图当中置为无效。
将该文件申请过的数据块在块位图当中置为无效。
因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。

为什么拷贝文件的时候特别慢,而删除文件却特别块?

因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效,无需真正的删除文件,所以拷贝慢,而删除文件时很快的。

如何理解目录?

目录也是文件,

目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小,目录的拥有者等。

目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode映射关系。

注意:每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名与文件inode的映射关系存储在其目录文件的文件内容当中,目录通过文件名和文件的inode之间的映射关系即可将文件名和文件内容及其属性连接起来
 

六.软硬链接

6.1软连接

可通过ln -s test.c test.cc

 软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。

 但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。

6.2硬链接

ln test.c test.cc

 我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,并且,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。

可以认为硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是多少,这里inode号为1841084的文件有test.c和test.cc,因此该文件的硬链接数为2。

与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个。

为什么刚创建的目录的硬链接数是2?

例如:

 原因:每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录。这个dir与.是一样的。它们的inode号也是一样的,是指同一个文件。

6.3区别

  1. 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
  2. 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值