Linux——文件(1)文件打开与关闭操作

我们知道,在Linux中,文件=内容+属性。而且,我们发现每次访问文件前,都必须先打开它(比如C中的fopen)但为什么必须打开才能访问呢?

假设我们现在写了一段文件写入的代码(fopen,fputs,fclose...)编译完成后形成可执行程序,运行后发现确实成功写入了内容。但文件是什么是时候被打开写入的呢?不是编写代码时,也不是写完代码的.c/cc文件,也不是编译后形成的可执行程序,只有当程序(进程)执行时,语句走到fopen函数才是真正打开文件。所以我们访问文件时是进程在访问。这种设计是因为:一个文件如果没有被打开,是放在磁盘中的。而我们要处理文件的信息等,需要把其交给CPU处理,但根据冯诺依曼体系中,我们的CPU是无法直接访问磁盘的,所以我们只能把文件放入内存中,才能被CPU调度处理。这个过程就需要进程访问文件使其进入内存部分。而且,一个进程也可以打开多个文件,这就意味着我们的操作系统也要管理在内存中的文件(先描述,再组织)。所以,一个文件,如果没被打开就放在磁盘,被打开就放在内存。

一、基本的文件写入

在C中我们常用fputs函数来实现写入文件的操作,在Linux中也可以

但是,当我们把message更改一下内容然后再次运行代码发现,之前写入的内容被覆盖成新的内容了。这和我们之前学过的echo类似,写入时,先把文件清空,然后进行新的写入。但echo可以追加写入(>>),文件是否也可以追加写入呢?可以。我们只需要把w打开方式换成a就可以(append)之前的内容就不会清空了。

二、如何利用文件流把内容打印在显示器

任何一个程序启动,进程都要默认打开三个文件流:stdin stdout stderr(标准输入 标准输出 标准错误)第一个我们称为键盘,后两个称为显示器。我们怎么通过这些文件流实现打印内容到显示器呢?

这些方式都可以实现,只不过就是把要写入的文件流改成写入到显示器的文件流中。但实际上我们并没有直接通过操作去访问硬件,是不允许的,我们所用的接口一定要封装对应的文件类系统调用。

三、接口所封装的系统调用——open

1.open的参数

 一共有两个open只是参数不同,第一个参数和我们的fopen是一样的,第二个叫标记位。打开文件的flags有如下几个

这些不同的选项之间是可以互相组合的。

标记位看似是一个整型,其实是位图。32个比特位分别代表不同的参数,这样我们就可以用一个参数的位置代表数量多的参数了,不用传好几个参数作为标记了,而上图的五个选项就是只有比特位为1的值,本质是宏。

我们通过参数和条件进行位运算来实现不同的功能

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

void print(int flags)
{
   //用位运算判断传参
   if(flags&ONE)
    {
       printf("one\n");
    } 
   if(flags&TWO)
    {
       printf("two\n");
    } 

   if(flags&THREE)
    {
       printf("three\n");
    } 
   if(flags&FOUR)
    {
       printf("four\n");
    } 
   if(flags&FIVE)
    {
       printf("five\n");
    } 
}

int main()
{
  //单独传参
  print(ONE);
  print(TWO);
  //一次传多个参数
  //打印ONE TWO
  print(ONE | TWO);
  //打印ONE TWO THREE
  print(ONE |TWO |THREE);

    return 0;
}
  

第三个参数,用来设定文件的权限,如果我们要写入的文件还没有创建,那么如果不传第三个参数它的权限就会是乱的,不是默认设定,所以我们要手动传参设定(传参方式和之前的chmod修改命令一样,用二进制数字代替不同身份的权限,比如666,建议数字前带上0,即传0xxx)如果文件已经存在就可以传前两个参数即可。(这里我们的权限也会受umask影响,也可以手动修改)

2.open的返回值

open的返回值是一个数字,我们称文件描述符,打开文件失败,返回-1.那这个文件描述符有什么用呢?我们再介绍一个系统调用close(),用于关闭文件,其参数就是我们的描述符。

3.其它系统调用

在打开和关闭期间,我们也可以完成,写,读等工作

值得一提的是,在完成写的操作时,是不进行文本清空的,直接覆盖。所以我们还要手动清空一下,只需要在open中再传一个O_TRUNC选项就可以了。还有其他选项,比如追加写入O_APPEND等

四、关于文件描述符fd

我们用变量接收打印看看到底是多少

结果发现是3,4,5,6.当我们重新启动一个进程然后只打开一个文件时发现是3。0,1,2哪去了?因为我们的进程启动时,默认启动了三个标准的输入输出流。我们的键盘显示器也被当成为文件打开了。

下面我们介绍一下文件到底是怎么被CPU获取的

我们知道,每启动一个进程,就会先形成一个PCB,但是,一个进程可以打开多个文件,我难道要把所有打开文件都放进内存然后直接访问吗?肯定不是,一定也是先描述再组织,每一个文件进入内存时,会有一个struct_file的封装,内部就记录着文件的相关属性等信息,但是难道每一个struct_file都对应一个PCB?那肯定不是,一定是一个PCB对应多个struct_file,体现了单个进程可打开多个文件。那么在task_struct中就一定有一个结构体指针struct files struct *files,里面有一个数组struct file* fd_array[],这个数组就是文件描述符表,每一个下标位置都代表一个打开的文件,那么0,1,2就是我们刚说的输入输出流,然后打开的文件就会从前向后找,直到找到空并放入。所以fd就是这个数组的下标,而且在系统层面,fd是访问文件的唯一方式。像我们的C中的FILE,fopen等接口都是进行了封装。

五、文件是如何写入和读取的

了解了系统调用write后,我们就可以用系统调用对文件进行写入操作了,但底层我们需要它到底是怎么完成的。

实际上,我们每一个文件的struct file中除了有自己的文件信息,还有自己的操作表,里面是函数指针集合(用来完成各种操作的函数),同时,还有一个文件的内核缓冲区,当我们用write写入时,本质是把write中的字符串拷贝到缓冲区,然后等关闭文件时,对文件缓冲区进行刷新即可完成写入,所以write其实是一个拷贝函数,这便是写入的过程。

读取的话,有时候文件的内容还没有来得及在缓冲区,可能就需要去磁盘把文件移动到内存的缓冲区然后read函数再进行拷贝、读取。总之,write和read函数本质上是进行拷贝操作的函数。

修改的本质,也是先读取(到内存),再写入。

六、重定向

我们再回到fd的使用

如果不加红框的close,输入的正常结果就是3,4,5,6。但是我们一旦加入这行代码,运行结果就变为,0,3,4,5。换成close(2)结果就是2,3,4,5。但是当我们换成close(1)时,运行发现,结果直接不打印了,什么都没有,这是为什么呢?

我们回归到printf和fprintf的参数上,其实fprintf只是比printf多一个文件流的参数,表示我们要指定打印在哪个文件上,而printf就是默认把内容打印在显示器(可以认为它的文件流参数固定为显示器,因为显示器也是一个文件),所以,如果我们把fprintf的文件流参数写为stdout,那么就和printf相同了。

这里的fd也一样,我们刚才说过,printf的文件流参数固定为stdout,底层是其绑定了stdout中fileno参数的1(文件描述符表的下标),所以当我们close(1),然后再打开其他文件,那么第一个文件就会占据1下标的位置(改变了1下标指针的指向,之前是指向显示器),但是这个过程上层函数是不知道的,它还是默认向1中去打印,所以我们看不到结果的原因是printf把内容打印在1下标的文件中了。这就是文件的输出重定向!

但是这种做法不太好,我们还有用系统调用来直接实现重定向的操作——dup2。

根据参数我们也能推断出dup2的传参就是把你想替换成的文件fd和目标位置的fd传入即可。假设我们想让1的指向3的文件就可以输入dup2(fd,1)(此时fd是3)。(实际是把“3”的fd拷贝一份,然后令“1”指向“3”)以下是具体应用。

补充一下:如果我们想实现追加重定向,可以把 图中的O_TRUNC(清空)选项换成O_APPEND(追加)。

接下来我们实现一下输入重定向

我们先看一下读取函数read

第二个参数是字符串指针(我们可以把我们的内容先放到数组然后把数组传过去),第三个参数就是你要读取的长度,我们一般用sizeof(array)即可。至于第一个参数,我们通过形参名字就知道其代表的意义,也就是文件描述符,默认为0,也就是键盘,我们输入什么它就会读取什么。那么输入重定向就是不从键盘读取,而是去其他文件获取内容,原理和输出重定向一样,用dup2调用,我们就不多解释了。

这和我们命令行的输入输出重定向效果相似,我们也可以结合使用,比如我们想让把程序在指定文件打印执行。

七、缓冲区

我们来看下面的代码

如果没有fflush(stdout),我们发现显示器上没有打印,同时文件内也没有内容,这是为什么呢?

我们知道,read函数是先向磁盘中的文件获取到数据然后把其放在文件内核缓冲区然后刷新进入内存,而这是一次调用系统调用。我们平时用的函数成本(时间与空间)实际上比不上系统调用的成本,因此我们要尽量减少系统调用的次数以提高效率,所以我们有另一个缓冲区——用户缓冲区。这个缓冲区的作用就是我们可以不利用系统调用(比如printf,fputs,fwrite等),先把数据拷贝到此缓冲区,等达到一定数量后进行刷新进行一次系统调用放入文件内核缓冲区,就不用多次系统调用了。我们做不到让内核缓冲区随时刷新,但我们可以控制用户缓冲区刷新。

我们现在来解释一下上面的代码(无fflush版本),首先printf并不是直接打印,而是先把内容放在了用户缓冲区内等待刷新,而当一个进程退出时,会自动刷新自己的缓冲区(所有的FILE对象内部),fclose(),即C语言的函数,关闭FILE时,也会自动刷新。但当代码走到close(fd1)时,进程还没有退出,我就把文件关闭了(即下标1的指针什么都没有指向了),此时进程关闭时就即不会刷新到显示器也不会刷新到文件中了,但如果我们把close改成fclose就可以成功刷新在文件中。

那这个fflush函数有什么用呢?我们运行图中的代码发现内容刷新到文件中了,也就是说,这个fflush函数是一个刷新用户缓冲区的函数并指定向哪个流刷新,所以还没等文件关闭,我们就即使的把缓冲区刷新到内核缓冲区了。

同时,我们也有让系统强制刷新在外设的方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值