本文由文件IO相关操作的一些操作,进一步详解了文件描述符fd,重定向,FILE结构体。
目录
一、C语言中的文件I/O操作
首先来回顾一下C语言中的文件I/O操作[C/C++]C语言中对文件的操作方法_RMA515T的博客-CSDN博客
之前的博客中就有详细的介绍。这里就只做简单演示。
#include <stdio.h>
int main()
{
FILE *fp = fopen("zht2", "w+");
if(!fp)
{
printf("erroe");
return 1;
}
int count = 5;
while(count--)
{
fwrite("hello\n", 6, 1, fp);
}
fseek(fp, 0, SEEK_SET);
char buf[1024];
while(1)
{
ssize_t s = fread(buf, 1, 7, fp);
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
C默认会打开三个输入输出流,分别是stdin, stdout, stderr,这三个流的类型都是FILE*, fopen返回值类型,文件指针。
文件的打开方式:
“r”(只读):为了输入数据,打开一个已经存在的文本。如果文件不存在则文件出错。
“w”(只写): 为了输出数据,打开一个文本文件。如果文件不存在则建立一个新的文件。
“a”(追加):向文本文件尾添加数据。如果文件不存在则出错。
“rb”(只读): 为了输入数据,打开一个二进制文件。如果文件不存在则出错。
“wb”(只写) : 为了输出数据,打开一个二进制文件。如果文件不存在则建立一个新的文件。
“ab”(追加): 向一个二进制文件尾添加数据。如果文件不存在则出错。
“r+”(读写): 为了读和写,打开一个文本文件。如果文件不存在则出错。
“w+”(读写): 为了读和写,建议一个新的文件。如果文件不存在则建立一个新的文件。
“a+”(读写): 打开一个文件,在文件尾进行读写。如果文件不存在则建立一个新的文件。
“rb+”(读写):为了读和写打开一个二进制文件。如果文件不存在则出错。
“wb+”(读写): 读和写,新建一个新的二进制文件。如果文件不存在则建立一个新的文件。
“ab+”(读写): 打开一个二进制文件,在文件尾读写。文件不存在则写建立一个新的文件。
二、系统文件I/O
除了上述C接口操作文件,,我们还可以采用系统接口来进行文件访问,
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("zht", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
printf("erroe");
return 1;
}
int count = 5;
while(count--)
{
write(fd, "hello\n", 6);
}
lseek(fd, 0, SEEK_SET);
char buf[1024];
while(1)
{
ssize_t s = read(fd, buf, 6);
if(s > 0)
{
write(1, buf, 6);
}
else
{
break;
}
}
close(fd);
return 0;
}
当前路径是指进程运行时所处的路径。
1. 接口介绍
open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,能且只能用一个。
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
write、read、close、lseek 可以类比C文件相关接口。
2. open函数返回值
open函数返回值是文件描述符fd,是一个整数。
在认识文件描述符fd之前,先来认识一下两个概念: 系统调用和库函数:
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)
open close read write lseek 都属于系统提供的接口,称之为系统调用接口
可以认为,f系列的函数,都是对系统调用的封装。
三、文件描述符fd
通过对open函数的学习,我们知道了文件描述符就是一个整数。
文件是进程运行时打开的,在Linux中一切皆文件,时Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
文件是
文件是由进程打开的,一个进程可以打开多个文件,文件被操作系统通过文件管理统一进行管理。
这里涉及到一个文件的相关概念,磁盘文件和内存文件,磁盘文件就是我们保存在磁盘上的文件
,由内容和元属性构成的。内存文件,更多的是文件的属性,在缓冲区延后式的慢慢加载数据,是操作系统直接读取的。
所谓文件描述符本质就是fd_arry数组的下标。
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体,表示一个已经打开的文件对象。
进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针。
本质上,文件描述符就是该数组的下标。只要有文件描述符,就可以找到对应的文件。
四、文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("file", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是 3。
关闭0:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("file", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果是: fd: 0
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
五、重定向
那如果关闭1:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("file", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么呢,如图所示:
重定向的本质是修改文件描述符fd下标对应的struct file* 的内容。
六、dup2 系统调用的使用
函数:
#include <unistd.h>
int dup2(int oldfd, int newfd);
使用示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
for (;;) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read");
break;
}
printf("%s", buf);
fflush(stdout);
}
return 0;
}
七、FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的,所以C库当中的FILE结构体内部,必定封装了fd。
首先,我们明确一下fopen在做什么:
1.给调用的用户申请struct FILE结构体变量,并返回地址;
2.在底层通过open打开文件并返回fd,把fd填充进FILE变量中的FILENO
再来明确缓冲,缓冲分三种:
1.无缓冲;
2.半缓冲(对显示器刷新数据);
3.全缓冲(对文件写入时);
来一段代码:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.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;
}
运行出结果:
如果对进程实现输出重定向./a.out > file , 结果变成了:
我们发现printf 和fwrite (库函数)都输出了2次,而write 只输出了一次(系统调用)。
这是因为:
C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf、fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后,但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲。
在FILE结构体中