【Linux】基础IO(open、文件描述符、缓冲区)

1、从文件操作开始

在C语言阶段,接触了很多库函数,如fopen、fclose、fread和fwrte,这些函数帮助了程序实现了内存与磁盘的输入输出功能。

不过之前都是停留在用户层面,在Linux中我们需要关注在系统层面IO发生了哪些细节。

一个有fopen,fclose等调用的程序,代码编译后,形成二进制可执行程序,但是没有运行,这时候的文件操作是肯定没有被执行的,所以对文件的操作,肯定是进程和被打开文件的关系!

一个文件要被访问,一定要先打开。本章探讨的都是被打开的文件

1.1 文件操作的系统调用接口

文件都是在磁盘的,磁盘作为硬件受到操作系统管理,用户操作想访问磁盘就不能绕过操作系统,就需要使用操作系统的接口,使用文件级别的系统调用接口,而操作系统只有一个。

综上,库函数底层必须调用系统接口,库函数可以千变万化,但是底层不变,所以了解底层能降低我们的学习成本。

open()

在这里插入图片描述
open是用来打开文件的,其中:

pathname参数是打开文件所在的路径。
mode参数是让我们对权限进行设定的。
flags作为一种标记位,通过bit位传递选项。
open返回值是一个文件描述符,这个在后面会说。


flags用法类似这种,通过对应宏可使得文件有对应一个或多个操作。

#include <stdio.h>

#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)

void show(int num)
{
	if(num & ONE) printf("ONE\n");
	if(num & TWO) printf("TWO\n");
	if(num & THREE) printf("THREE\n");
	if(num & FOUR) printf("FOUR\n");
}

int main()
{
	show(ONE);
	printf("\n");
	show(ONE | TWO);
	show(ONE | TWO | THREE);
	show(ONE | TWO | THREE | FOUR);
	return 0;
}

使用open:

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

int main()
{
	umask(0);
	//int fd = open("./test", O_WRONLY | O_CREAT);
	int fd = open("./test", O_WRONLY | O_CREAT, 0666);
	if(fd < 0)
	{
		perror("open fail");
		return -1;
	}
	close(fd);
	return 0;
}

在flags中:

  1. O_RDONLY 代表只读
  2. O_WRONLY 代表只写
  3. O_RDWR 代表读,写打开
    (以上三个常量,必须指定一个)
  4. O_CREAT 若文件不存在,则创建它
  5. O_APPEND 追加写
  6. O_TRUNC 每次打开文件空白刷新文件

对于mode参数,如果没有初始权限,产生的文件权限就是乱的,没办法正常写入(读取就可以不用),所以一定要初始化权限,而shell中有对应umask会干扰我们设置的权限,我们在程序中可以通过umask(0)避免这种干扰。
在这里插入图片描述


write和read
在这里插入图片描述
write将buf中count字节大小写入fd中。
read将fd中count字节大小写入buf中。

在这里插入图片描述
lseek改变文件的读写位置,参数如下:

fd:文件描述符
offset:偏移量
whence:通过bit位传递选项。

offse和whence需要一起使用。
当whence是
SEEK_SET,偏移量将从文件的头开始算再加上offset。
SEEK_CUR,偏移量从当前位置加上offset。(offset可以是负数)
SEEK_END,偏移量从文件末尾开始加上offset。(offset可以是负数)

通过以上功能就可以通过系统调用完成文件操作了。

   #include <stdio.h>
   #include <unistd.h>
   #include <sys/types.h>
   #include <sys/stat.h>
   #include <fcntl.h>
   #include <stdlib.h>
   #include <string.h>
   int main()
   {
      int fd = open("./test", O_RDWR | O_CREAT, 0666);
      if(fd < 0)
      {
          perror("fd fail\n");
          exit(-1);
      }
  
      const char str[] = "hello world!\n";
      ssize_t ret = write(fd, str, strlen(str));                                                                                                                                                                                                                            
      if(ret < 0)
      {
          perror("write fail\n");
          exit(-1);
      }
 
      lseek(fd, 0, SEEK_SET);
      
      char buf[64] = {0};
      ret = read(fd, buf, 63);
      if(ret < 0)
      {
          perror("read fail\n");
          exit(-1);
      }
      printf("%s", buf);
      close(fd);
      return 0;
  }

1.2 文件描述符

打开文件后返回的fd是什么?在官方文档中,说返回新的文件描述符。
在这里插入图片描述

   int main()  
   {
      int fd = open("test", O_RDWR | O_CREAT, 0666);
      
      printf("fd:%d\n", fd);                                                                                                                          
  
      close(fd);                                                                                                                             
      return 0;                                                                                                                              
  } 

在这里插入图片描述
那这个所谓的文件描述符是什么呢?
需要再回到理解文件中:

进程是可以打开多个文件的,当系统有大量被打开的文件时,系统需要管理这些文件,所以系统需要给这些文件创建对应的内核结构体表示文件。

其中struct file{} 表示一个个被打开的文件,内部有着文件的属性。
所有被打开的文件struct file{} 通过对应数据结构串起。
那么进程是如何和文件联系起来的呢?
在进程对应PCB中有struct files_struct* files,这个指针指向一个文件描述符表files_struct,这个表最重要的部分就是包含了一个指针数组,每个元素都是一个指向打开文件的指针。
在这里插入图片描述
而文件描述符就是该fd_array指针数组里的下标,拿着文件描述符就能找到对应的文件。
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。

   int main()
   {
      printf("stdin:%d\n", stdin->_fileno);
      printf("stdout:%d\n", stdout->_fileno);
      printf("stderr:%d\n", stderr->_fileno);                                                                                                         
      int fd = open("test", O_RDWR | O_CREAT, 0666);
  
      printf("fd:%d\n", fd);
  
      close(fd);
      return 0;
  }

在这里插入图片描述
所以这也是为什么fd是3的原因。

其实将标准IO流文件描述符的打印再结合这张图
在这里插入图片描述
可以很清楚的知道C语言的文件描述符,其实就在FILE结构体中定义。

总结:

  1. 在系统层面,文件描述符其实就是PCB中指向的文件表中记录打开文件的索引。
  2. 在语言层面,在特定的文件类里也一定会有一个字段表示文件描述符(如C语言的FILE结构体)。

2、重定向

首先是文件描述符的分配规则
从小到大,按照遵循寻找最小的没有被占用的fd。
也就是在默认打开0,1,2后,如果关闭1,再打开文件,文件的fd就会占用1。

理解了分配规则,再看看close()
在这里插入图片描述
关闭对应文件描述符。

   #include <stdio.h>
   #include <unistd.h>
   #include <sys/types.h>
   #include <sys/stat.h>
   #include <fcntl.h>
   
   int main()
   {
      close(1);
      umask(0);
      int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
      if(fd < 0)
      {
          perror("open");
          return 1;
      }
      printf("fd: %d\n", fd);
      fflush(stdout);
      close(fd);
      return 0;
  }

关闭1号文件描述符关闭标准输出,再打开文件,文件就被数组1下标指向了。
printf调用stdout,stdout内核对应文件描述符还是1,但是文件描述符表数组中fd内的指针指向打开的文件。

那么后续打印就打印到文件中了。
在这里插入图片描述

这种操作也称为
重定向: 上层用的文件描述符不变,在内核中更改文件描述符对应的文件信息操作不同文件。


另一个方法通过系统调用dup2实现
在这里插入图片描述
将oldfd的内容拷贝到newfd中。
成功返回oldfd,失败返回-1。

   #include <stdio.h>
   #include <unistd.h>
   #include <sys/types.h>
   #include <sys/stat.h>
   #include <fcntl.h>
   #include <string.h>
   
   int main()
   {
      umask(0);
      int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
      if(fd < 0)
      {
          perror("open");
          return 1;
      }
      dup2(fd, 1);
      printf("fd: %d\n", fd);
      fflush(stdout);
      close(fd);
      return 0;
  }

在这里插入图片描述
在这里插入图片描述

3、缓冲区

首先得知道为什么要有缓冲区?

就如我们寄快递一样,物流公司是位了节约我们的时间。
进程将数据读写到磁盘,为了节约进程进行数据IO的时间,会先将数据写入到缓冲区。
(进程在内存,如果让进程直接访问外设,它的速度就会变的很低)

数据的写入,在底层实现其实就是数据的拷贝,将数据从进程拷贝到缓冲区或者外设中!

缓冲区的刷新策略

首先以下策略都是在C语言层面上的:
a.无缓冲 直接刷新到磁盘 不怎么用。
b.行缓冲 一行一行刷新,为了适应用户(比如显示屏)。
c.全缓冲 缓冲区满后全部刷新(比如磁盘)。

也有特殊情况
1.用户使用fflush强制刷新
2.进程退出刷新

先看一个现象

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg0="hello printf\n";
	const char *msg1="hello fwrite\n";
	const char *msg2="hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

在这里插入图片描述
为什么在重定向到文件后,系统函数打印一遍,而与库函数相关的函数打印了两遍?
首先这个问题出现在缓冲区,其次如果出现在缓冲区,这个缓冲区一定不在系统层面,不然write也会打印两次。

实际上,在没有>之前,C库函数数据打印在屏幕上,属于行缓冲,所以一句一句直接打印。
在>之后,C库函数是打印在文件中,这时候就成了全缓冲,打印在stdout缓冲区中的还没到一定量,所以还在缓冲区里,fork之后创建子进程,随后进程立即退出,刷新了缓冲区(做了修改),所以会出现写时拷贝,C库函数就打印了两遍。

系统接口函数write没有FILE,不存在该缓冲区,所以直接打印。

这个缓冲区在哪?
其实缓冲区就在FILE结构体中
在C语言中(用户级别中),缓冲区其实就在FILE结构体内。
在这里插入图片描述

缓冲区和系统之间

其实在语言层面上的数据放入缓冲区后不是直接的放入磁盘。
在放入缓冲区后,会经过一个内核缓冲区,这里面的刷新策略由OS自主决定,随后刷新到磁盘。

不过为了避免在一些特殊情况造成数据丢失,系统调用接口fsync()可以使得数据直接从缓冲区放到磁盘。
这里是引用

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值