Linux:文件操作

目录

一、关于文件

1、文件类的系统接口

2、文件的含义

二、文件操作

1、C语言文件相关接口

2、系统接口

open

close

write

read

三、文件描述符

关于fd

fd的分配规则

输出重定向示例

输入重定向示例

追加重定向示例

dup2函数

缓冲区

stdout与stderr

perror


一、关于文件

1、文件类的系统接口

首先不同语言都有不同的文件操作接口,C语言、C++、java都有各自的文件接口,而这些文件接口的底层,其实是封装的系统接口

之所以各个语言不直接使用文件类的系统接口,是因为系统接口相比较各个语言封装出来的接口,难度较大,学习成本比较高,各种语言对系统接口做封装,是为了让接口更好的使用

其次,也是为了跨平台这个特性,如果每种语言对系统接口不封装,直接调用系统接口,那么所有的文件操作都必须使用操作系统接口,无法在其他平台运行,是不具备跨平台特性的

2、文件的含义

说起文件,大部分人都想的是普通的磁盘文件,例如记事本打开的文本文件、.exe之类的文件

其实站在系统的角度,能够被input读取,output写出的设备都叫做文件,例如:键盘、显示器、磁盘、网卡、显卡等外设,都可以被称为文件

二、文件操作

1、C语言文件相关接口

我们先在Linux中写C语言中学习的文件接口:

以w的方式打开,如果文件不存在,会创建一个新文件,那么这个文件会创建在哪呢

很显然会创建在当前路径下,而当前路径指:一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径,而这个工作路径就是当前路径

上面的代码中,test.txt这个文件是不存在的,该路径下只有两个文件:

所以在编译运行file.c后,test.txt会在该路径下创建出来:

由于当前进程的工作路径在这里,所以创建出来的test.txt也就在这个路径下了


以w方式打开文件时,不论文件里面是否有内容,打开文件时做的第一件事就是清空文件内容,哪怕我们什么都不干,只是打开关闭文件,文件内容都会被清空,下面举个例子:

我们向文件test.txt中写入"hello"

查看test.txt内容:

这时改变file.c的内容,将fprintf的相关代码删除:

然后再编译运行myfile:

发现test.txt里面的内容没有了,大小为0


有了上面打开文件就会清空文件内容的基础,再看下面的例子:

先cat test.txt,发现文件里没有内容

接着echo输出重定向的方式向test.txt写了一个字符串hello,cat test.txt发现确实写入了

然后执行>test.txt后,再cat,发现又被清空了

这里的>符号,可以当做一个命令,表示要往文件里写入的,而向文件写入首先做的就是打开这个文件,上面也提到了,打开文件做的第一件事就是清空文件内容,所以我们就可以明白为什么执行完>test.txt后,test.txt中内容就被清空了

以上就是复习的C语言相关文件接口


2、系统接口

上面提到的C语言的库函数,底层一定会调用系统调用接口

系统调用接口有open、close、write、read

open

通过man了解一下open:

有三个头文件需要包含,然后看下面的两个open后面的参数

参数pathname:代表需要打开文件的路径 + 文件名

参数flags:表示选项(O_RDONLY、O_WRONLY、O_RDWR必须包含这三个其中一个)

参数mode:表示权限(如果传入0666,就表示拥有者、所属组、other的权限都是rw-)

选项有很多,例如:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_CREAT(文件不存在就创建)、O_TRUNC(打开文件时如果有原本内容就清空原始内容)

像上面的这些是O_RDONLY、O_WRONLY、O_RDWR,其实就是宏定义,而open后面的参数只有一个flags,如何传递这么多的标记位,这里就和C++中的位图类似了,用比特位传递

关于open的返回值:

如果成功了,会返回一个文件描述符        

如果失败了,就会返回-1

关于两个open的使用场景:

三个参数的open是用于文件不存在,第三个参数可以控制该文件的权限

两个参数的open是用于文件已经存在的场景


下面示范一下如何用比特位传递多个标记位:

定义了三个宏A、B、C,分别是0x1(0000 0001)、0x2(0000 0010)、0x4(0000 0100)

main函数中给test传递的参数A | B,就是0000 0001 | 0000 0010即传递上去的值为0000 0011

这时传递上去的0000 0011与A(0000 0001),B(0000 0010)按位与都满足,所以会打印里面的语句

运行结果为:

满足传上去的参数要求

这里的例子就说明了,如果想传入多个标志位,则用 | (按位或)的方式就能做到,下面会用到这个结论


下面使用一下open:

首先第一个umask是一个函数,用于在当前进程重置umask值,以便于我们open中传递的第三个关于权限的参数受到系统中umask值的影响

open中的第二个参数,O_WRONLY | O_CREAT,O_WRONLY是以w的方式打开,O_CREAT是创建一个文件,如果文件不存在必须要加上O_CREAT选项,中间的|在上面的例子中说到过,传递多个标志位时用|连接

运行结果如下:

成功运行


close

通过man查看close

这里的close使用就很简单了,包含一个头文件unistd.h

关闭文件时,将刚刚返回的文件标识符传入即可

代码中添加close即可:


write

通过man查看write

包含一个头文件unistd.h

参数的含义就是:向一个fd里,写特定的buf,字符个数是count个

下面演示用法:

我们给了一个const char*类型的字符串,使用write接口

运行结果如下:

成功向文件中写入了I am write

但是这里会有一个问题,我们在C语言中,写入文件时,如果文件中有内容,再重写其他内容时会被覆盖,而在Linux中则会存在问题,如下:

将字符串str改为"111",然后在运行,查看现在test.txt中的内容:

可以看到,并不是我们所想的111,而是111覆盖了刚刚内容的前三个字符的位置,反而test.txt中的内容变为了111m write

想解决这个问题,只需要加上选项O_TRUNC,即创建文件时如果有原始内容,则清空原始内容

如下所示:

此时再重新运行,打印test.txt中内容就变为我们想要的样子了:

这里也应证了我们最开始所说的文件类的系统接口学习成本比较高,难度比较大,所以各种语言会封装文件接口,使用起来更简单些

我们使用的   open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666)

与C语言中的fopen("test.txt","w")的功能是一样的,更看出其中的难易

上面用的是w的功能,如果是a(追加),只需将O_TRUNC换为O_APPEND即可:

这时多运行几次,结果为:

这时代码的open("test.txt",O_WRONLY|O_CREAT|O_APPEND,0666)

与C语言的fopen("test.txt","a")功能相同


read

通过man查看read

同样包含一个头文件unistd.h

参数的含义就是:从特定的文件描述符fd读取数据,读到buf里面,读取的个数是count个

我们先向text.txt中写入数据:

接着使用read:

首先将open后面的参数变为O_RDONLY,只读,然后创建一个buf的字符数组,memset将buf中的数据全部置为'\0',read读文件,最后打印出读取的内容:

最终读取成功


三、文件描述符

关于文件描述符fd,上面的代码中有打印过,接下来将代码做以改造,多打开几个文件,再观察文件描述符:

创建4个test.txt文件,观察每次返回的文件描述符fd:

可以看到test.txt1~4的文件描述符分别是3、4、5、6,那么为什么是从3开始的,0、1、2是对应的什么,其实在C语言中我们就学过了,0、1、2分别对应的是stdin(标准输入)、stdout(标准输出)、strerr(标准错误),是被默认打开的

stdin、stdout、strerr他们的类型都是FILE*的


下面举例验证一下0、1、2这三个文件描述符:

我们C语言中使用的fprintf,如果要往显示器上写,第一个参数就是stdout

而系统接口write,第一个参数是fd,我们可以传入1,观察是不是往显示器上写入:

结果为:

可以发现,传入文件描述符1,也是往显示器上写入的,与stdout功能相同


下面示例文件描述符为0的stdin

系统接口read的第一个参数传入0,表示stdin

read的返回值是读入的个数,如果大于0,就说明读入成功了

结果为:

可以看到,也可以通过传入文件描述符0,实现stdin的功能


关于fd

fd的本质是指针数组的下标

在内核中,每一个进程都是可以打开多个文件的,也就是一对多的关系

而为了管理这些文件,操作系统需要构建一个file结构体,包含了被打开文件的几乎全部内容,如果被打开的文件很多,就用双链表组织起来

而我们刚刚说到的0、1、2,就是一个指针数组的下标,表示这个指针数组前三个位置存储的就是stdin、stdout、stderr的位置,Linux进程默认会打开这三个文件描述符,0、1、2对应的物理设备一般是:键盘、显示器、显示器

这里的指针数组中的每一个元素都是一个指向打开文件的指针

而我们上面所举例子中新创建了4个test.txt文件,他们返回的fd分别是3、4、5、6,也就代表着他们所在的位置就是这个指针数组的3、4、5、6下标,在数组中做哈希索引,索引之后就能找到被打开的文件对象

进程打开一个文件,操作系统会创建一个struct file对象,然后在该进程的指针数组中,分配一个没有被使用的数组空间,接着把新的文件地址填在这里面,然后返回对应的文件描述符下标给用户使用,所以fd在内核中,本质是一个指针数组的下标


fd的分配规则

fd的分配规则是:优先最小的,没有被占用的文件描述符

正常情况下,我们创建一个新文件,文件描述符是3,因为0、1、2是默认被打开的

结果为:

如果我们提前把文件描述符0关了(close),那么新创建一个文件会被分配到哪个文件描述符:

这时结果为:

所以可以明白,文件描述符基本的分配规则就是:从头遍历,找最小的且没有被占用的文件描述符


输出重定向示例

那么如果close的是1呢,如图:

创建test.txt前,close(1),观察现象:

我们运行myfile,没有任何内容,但是cat查看test.txt时却发现,本该打印在显示器上的内容出现在了test.txt中

这是因为,在我们创建test.txt前,关闭了文件描述符1,也就是系统默认打开的stdout,它所对应的指针数组下标为1的位置为空了,这时创建test.txt,按照fd的分配原则,将文件描述符1就分配给了test.txt

我们所使用的printf、fprintf中,都是包含stdout的,所以执行时默认就是执行了stdout,而stdout本身默认的文件描述符就是1,刚刚分配文件描述符是底层做的,上层并不清楚,所以它们就照常往1里面写,但是写完后,通过进程找到指针数组,再通过指针数组的下标1找到所对应的文件,而这时1所对应的文件早已不是stdout,而是test.txt,因此我们printf的内容都在test.txt中可以看到

本来应该显示到显示器的内容,被写入到了文件中,这就是输出重定向

以上所说的就叫做重定向的原理!

重定向的本质:其实是在操作系统内部,更改fd对应的内容的指向


输入重定向示例

先正常使用fgets函数,观察正常打印的结果:

创建了一个test.txt文件,理应分配的文件描述符是3

并且下面的fgets第三个参数是stdin,是从键盘读取的

所以先打印fd:3,然后在键盘输入hello,显示器就打印了一个hello

上面是正常情况,下面演示输入重定向的情况,即close(0)时的情况:

首先我们将test.txt的内容更改为hello test

然后在open文件test.txt前,close(0):

这时根据fd的分配规则,显然test.txt的文件描述符就分配为了0

运行结果为:

可以看到,打印出fd:0,但是接下来本应该从键盘读取,却变为了直接读取文件test.txt的内容

原因也很简单,我们close(0)后,原本数组下标为0的的位置是指向stdin的,现在变为了指向文件test.txt,而上层并不知道底层做了这些改变,在执行fgets时依然是从文件描述符0中读取,而这时文件描述符已经变为了test.txt,所以执行时,根据文件描述符0指向的地址是test.txt的位置,并不是stdin的,所以不需要等待从键盘输入再打印,而是直接打印test.txt的内容

而上面所说的,本来应该从键盘中读取的内容,变为了从文件中读取,这就是输入重定向的定义!


追加重定向示例

这是上面所举例的输出重定向

追加重定向只需将open中的参数O_TRUNC(清空)改为O_APPEND(追加)即可

这时看结果:

第一次执行,打印了原有的内容hello test,且追加了fd:1

又对执行了两次,可以看到test.txt的内容也多了两行内容

这就是追加重定向


dup2函数

而我们如果想实现重定向,上面的方法可以是可以,但是比较麻烦,下面说说更好的方法去实现重定向

首先学习函数dup2

dup2中有两个参数oldfd和newfd

关于这两个参数需要详细说明:

oldfd是要拷贝到newfd中去,所以最终会留下oldfd

所以如果我们创建一个文件,这个文件的文件描述符就是3,这时要进行输出重定向,即将文件描述符1(stdout[显示器])改变指向,所以需要将3拷贝到1中去,最后留下的就是3,即所创建的文件了

所以实现输出重定向使用dup2时,oldfd是3,newfd是1,具体示例如下:

使用fprintf时顺便复习一下命令行参数,这时结果为:

如果我们直接执行myfile这个可执行文件,就进入第一行语句if (argc != 2)中了,因为此时只有myfile,argc == 1

接下来我们在myfile后面跟上了一个字符串,这时就符合要求,也打印出来了我们输入的字符串

以上是不使用dup2的情况,下面使用dup2进行输出重定向:

上面说到的,第一个参数是oldfd,第二个参数是newfd,要保留oldfd,所以我们传入fd,即test.txt的fd

观察结果:

dup2函数实现了输出重定向


dup2函数追加重定向:

只需将open的第二个参数中的O_TRUNC变为O_APPEND即可:

结果为:

也能很轻松地实现


缓冲区

缓冲区就是一段内存空间

有缓冲区能够提高整机的效率,也能够提高用户的响应速度

缓冲区的刷新策略:

1、立即刷新

2、行刷新(行缓冲,例如遇到\n)

3、满刷新(全缓冲)

也存在特殊情况:

1、用户强制刷新(例如fflush)

2、进程退出

一般采用行缓冲策略的设备文件是显示器,采用全缓冲策略的是磁盘

而所有的设备都是倾向于全缓冲的,因为缓冲区满了才刷新,IO操作少,访问外设的次数变少,从而提高效率

设备是倾向于全缓冲,但是其他的刷新策略,都是根据具体情况所做出的改变:

例如显示器是行缓冲刷新,是因为显示器是直接给用户看的,需要照顾用户体验


下面看一个例子:

先执行printf、fprintf、fputs,然后执行write,最后调用fork()

在调用fork()时,上面的函数已经执行完毕了

下面观察结果:

编译后直接运行myfile,发现在显示器上打印的是4行

但是向普通文件打印时,却变成了7行

通过观察可以发现,C语言的接口是打印了两次的,而系统接口只打印了一次

通过这个结果,我们可以看出来,执行fork()后,并不影响系统接口,所以如果存在缓冲区,是由C标准库提供的,而不是操作系统提供,因为如果是操作系统提供的,那么上面的结果应该是一样的,write也应该打印两次

所以如果我们使用fprintf、fputs等接口,应该是先写入C标准库的缓冲区中,然后我们就不需要关心了,何时刷新都是由C标准库管理的

而如果使用的是write接口,就会直接将数据写给操作系统,不需要经过缓冲区


下面说明上面代码在显示器上打印,和重定向到文件中,为什么结果不一样的原因:

第一,如果向显示器打印,这时的刷新策略是行刷新,每遇到一个\n就刷新一次,那么最后执行fork的时候,一定是函数执行完了并且数据已经被刷新了,此时的fork无意义!所以在显示器上打印时,只有4个结果

第二, 如果你进行了重定向,就变为了要向磁盘文件打印,这时的刷新策略变成了全缓冲!所以printf、fprintf、fputs字符串后面的\n便没有意义了,此时执行fork函数,有父进程和子进程,而父进程的数据还在缓冲区当中,fork函数后面就执行return 0即进程退出

上面说到特殊情况,进程退出时会刷新缓冲区,而刷新的过程其实就是写入的过程可以理解为一个进程退出时为了不影响另一个进程使用现有的数据,所以会发生写时拷贝,所以我们明白了,上面凡是在C标准库的缓冲区的数据,都出现了两份,而直接直接写给操作系统的系统接口,只有一份

上面了解到,之所以打印到显示器上内容和打印到文件里的内容不一样,是因为打印到文件里时,改变了刷新策略,变为了全缓冲,导致父进程的数据没有被刷新出来,还保存在缓冲区中,所以执行fork后,发生写时拷贝,打印出来了2份数据

那如果我们在fork前,执行fflush函数,强制刷新缓冲区,这时执行fork时,缓冲区中没有数据,是不是就像我们所推测的那样,上面两种方式打印的内容就相同了,如下图:

观察结果:

通过观察可以发现,执行完fflush后,两种方式的打印的内容就完全相同了


stdout与stderr

stdout与stderr对应的文件描述符分别是1和2

而1和2对应的都是显示器文件,但是它们是不一样的

在输出或追加重定向时会有区别:

我们分别在打印时加上->区别是1还是2

下面看结果,正确运行时,都能打印到显示器上:

如果输出重定向:

我们会发现,往2号文件描述符写的内容依旧打印到显示器上了,而只有往1号文件描述符写的内容才重定向到文件中

所以我们明白了,这里的重定向其实是往1号文件描述符写的

1和2对应的都是显示器文件,可以理解为打开了两份,重定向时把正确的信息写到文件里,错误信息依旧打印到显示器上

因此一般情况下,如果程序可能存在问题,建议使用stderr或cerr来打印

如果是常规的文本文件,建议使用stdout或cout打印


针对标准输出和标准错误,如果我们想将这两种分开打印,可以进行如下操作:

这时就可以将调用stdout的打印到right.txt中,调用strerr的打印到err.txt中了

我们可以将标准输出和标准错误分开,2>err.txt就是将本来应该显示到2号文件描述符的内容显示到err.txt上

那如果我们就是想将stdout和stderr的内容都放在一个文件中,就可以进行如下操作:

这时就能实现上述要求


perror

上面代码中出现了perror这个函数,其实我们可以发现使用perror后,打印结果多了一个success,其实这个函数会自动根据全局的错误码,输出对应的错误原因

例如我们使用open时,经常在下面的if语句里,使用perror("open"),就是表示,如果open失败了,可以打印失败的错误信息

假设我们使用open,去读test文件,if语句中使用perror

打印结果如下:

通过ls可以发现,当前路径下并没有test文件,所以open的返回值小于0,即进入if语句执行perror

通过结果可以看到,perror会自动调用错误码输出错误信息,例如上述例子的错误信息就是不存在该文件


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值