目录
一、关于文件
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会自动调用错误码输出错误信息,例如上述例子的错误信息就是不存在该文件