【Linux基础I/O一】文件描述符和重定向
- 1.C语言的文件调用接口
- 2.操作系统的文件调用接口
- 2.1open接口
- 2.2close接口
- 2.3write接口
- 2.4read接口
- 3.文件描述符fd的本质
- 4.标准输入、输出、错误
- 5.重定向
- 5.1什么是重定向
- 5.2输入重定向和输出重定向
- 5.3系统调用的重定向dup2
- 6.缓冲区
1.C语言的文件调用接口
- fopen
FILE *fopen(const char *filename, const char *mode);
filename 参数是一个字符串,指定要打开的文件名或路径
mode 参数是一个字符串,指定文件的打开模式,常见如下:
“r”:只读,若文件不存在则报错
“w”:只写,若文件不存在则创建,打开时清空文件原有内容
“a”:只写,若文件不存在则创建,打开时从文件末尾追加
返回值:
如果成功打开文件,则返回指向 FILE 类型结构的指针,该指针用于后续的文件操作。
如果打开失败,返回 NULL,并且通过检查 errno 变量可以确定失败的具体原因
FILE *fp = fopen("temp.txt", "r");
if (fp == NULL) {perror("Error opening file");return 1;
}
- fclose
int fclose(FILE *stream);
stream 是一个指向 FILE 结构的指针,指定要关闭的文件
返回值:
如果成功关闭文件,则返回 0。
如果关闭失败,则返回 EOF
FILE *fp = fopen("temp.txt", "r");
if (fp == NULL) {perror("opening error");return 1;
}if (fclose(fp) == 0) {printf("close succeed.\n");
} else {perror("closing error");return 1;
}
- fprintf
int fprintf(FILE *stream, const char *format, ...);
stream是一个指向FILE结构的指针,指定要写入的目标文件
format是一个格式化字符串,类似于printf函数中的格式化字符串,用于指定输出的格式
返回值:
成功返回写入的字符数,如果出错则返回一个负数
默认打开文件的时候,清空文件内容
a打开方式:append追加方式写入文件,不会清空原文件内容
2.操作系统的文件调用接口
2.1open接口
pathname 是一个字符串,表示要打开的文件路径
flags 是打开文件的标志位,例如O_RDONLY
(只读)、O_WRONLY
(只写)、O_RDWR
(读写)、O_CREAT
(若打开的文件不存在,则创建该文件)
mode 是文件的权限,通常与 flags 参数中的某些标志结合使用,用于指定文件的创建模式
返回值:
如果成功,返回一个新的文件描述符,用于后续的文件操作
如果出错,返回 -1
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT);return 0;
}
运行./mybin后,当前目录就出现了文件log.txt了,但是它的权限值是乱码-r-s–x–T,如果你删掉该文件后再次运行代码,你会发现下一次的权限值和上次还不一样
此时就需要三个参数的open接口:
mode用于控制文件的初始权限,一般系统默认掩码是0002
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}
open的第三个参数传入0666,也就是log.txt的初始权限
由于umask是0002,所以最终权限为664也就是rw-rw-r–
2.2close接口
作用:关闭文件
描述符fd对应的文件, 调用成功返回0
2.3write接口
fd:被写入文件的fd
buf:指向被写入的字符串
count:写入字符的个数
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);char* str = "Hello\n Hello\n";write(fd, str, 6);close(fd);return 0;
}
向log.txt写入了一个字符串str,write的第三个参数为6,只写入了6个字符Hello\n,输出结果是Hello
在保留原先log.txt的情况下,更改test.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);char* str = "Hello\n Hello\n";write(fd, str, 6);close(fd);return 0;
}
log.txt的内容变成了123lo,为什么?
以O_WRONLY模式对文件进行写入时,不会先把文件内容清空,而是直接从头开始覆盖
有两种解决方案,对应两个open的选项
O_TRUNC
打开时清空文件内容O_APPEND
以追加的形式写入
2.4read接口
fd:目标文件的文件描述符fd
buf:将文件内容读取到buf指向的空间中
count:最多读取count个字节的内容
返回值:
< 0:读取发送错误
= 0:读取到文件末尾
0:读取成功,返回读取到的字符个数
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>int main()
{int fd = open("log.txt", O_RDONLY);char buffer[1024];ssize_t ret1 = read(fd, buffer, 1024);printf("ret1 = %ld\n", ret1);printf("%s", buffer);close(fd);return 0;
}
在系统创建一个log.txt文件,内容为字符串hello Linux
一开始用open以只读RDONLY形式打开文件,通过read(fd, buffer, 1024)
把内容存储到数组buffer中,并把返回值交给ret1,随后输出ret1和buffer
第一次read返回值为12,也就是字符串Hello Linux有12个字符
3.文件描述符fd的本质
文件描述符的本质是一个数组的下标
在Linux系统下,一切皆文件
文件 = 内容 + 属性
文件操作 = 对内容的操作 或 对属性的操作
因为冯诺依曼体系结构,CPU只和内存做交互,所以要对文件做修改就需要加载到内存
我们会同时打开多个文件,也会有别人同时打开文件,所以,操作系统需要管理这些打开的文件
管理 = 先描述,再组织
操作系统同样会像管理进程那样,也给文件创建对应的结构体,然后文件之间会有一些连接关系
这样,对文件的管理就变成对某种数据结构的管理
文件操作是用户让操作系统进行的,操作系统会为此创建进程
所以文件操作实质是进程和被打开文件的关系
操作系统运行时,会有很多进程在运行
在文件没有被打开之前,文件是存在磁盘中的
打开文件是进程打开的:进程将文件的数据加载到文件内核级的缓存块中
而一个进程能打开很多文件,同时,系统当中可以存在许多进程
为了更好的管理文件,所有被打开的文件,操作系统都会对其创建一个结构体struct file
文件 = 属性 + 内容,而这个struct file结构体对象就是用来描述被打开的文件
被打开的文件结构体对象,被操作系统用双向链表组织起来,成为一个双向链表
于是,操作系统对被打开文件的管理就变成了对一个双向链表的增删查改,解决了对文件的管理问题
文件和进程之间的关系如何描述和组织?
进程的PCB(task_struct)结构体对象中存在一个指针变量:struct files_struct *file
在Linux 2.6.10内核中,struct files_struct如下:
其最后一个成员fd_array
是一个数组,指向的成员类型为struct file*
,也就是指向struct file的指针
struct files_struct {atomic_t count;spinlock_t file_lock;/* Protects all the below members. Nests inside tsk->alloc_lock */int max_fds;int max_fdset;int next_fd;struct file ** fd;/* current fd array */fd_set *close_on_exec;fd_set *open_fds;fd_set close_on_exec_init;fd_set open_fds_init;struct file * fd_array[NR_OPEN_DEFAULT];
};
操作系统全局管理的struct files_struct
,整个系统中所有被打开的文件都要被这个结构体管理
每个进程自己打开维护的struct files_struct
,分别管理自己打开的文件
这个指针数组内部的一个个指针就对应着一个个被打开的文件的地址
而文件描述符fd就是这个数组的下标
所以,当用户想要访问一个进程下的文件时,因为每一个进程的PCB是唯一的,所以文件管理数组也唯一的,只要是进程打开了一个文件,就会把文件的*file
添加进去
只需要返回这个进程PCB结构体中fd_array
数组的下标
就可以找到对应的文件了
下述三个系统调用函数都有一个int fd,当打开一个文件时,
要对这个文件进行写、读、操作,都需要传递open时返回的文件描述符fd
用fd去找到对应的文件然后执行相关操作
对文件的读写等操作的数据改动都是在缓冲区内进行
然后由操作系统刷新到磁盘对应位置
所以,读取文件,本质上就是从文件的文件内核缓冲区内加载数据到应用层的用户级缓冲区
4.标准输入、输出、错误
0:标注输入 (键盘)
1:标准输出 (显示器)
2:标准错误 (显示器)
int fd1 = open("text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("text4.txt",O_WRONLY | O_CREAT);printf("fd1 = %d\nfd2 = %d\nfd3 = %d\nfd4 = %d\n",fd1,fd2,fd3,fd4);
确实多出了4个,文件文件描述符从3开始,编号0 1 2的fd去哪里了?
C语言中,会为我们默认打开三个流stdin
标准输入,stdout
标准输出,stderr
标准错误,0、1、2为编号的fd就已经被占用
C语言中,文件流是以FILE的形式被管理的,毫无疑问FILE是对Linux文件系统的封装,FILE内部一定存储了fd,否则无法通过fd来访问特定的文件,其中FILE的_fileno成员就是fd
#include <stdio.h>int main()
{int fd1 = stdin->_fileno;int fd2 = stdout->_fileno;int fd3 = stderr->_fileno;printf("stdin->_fileno = %d\n", fd1);printf("stdout->_fileno = %d\n", fd2);printf("stderr->_fileno = %d\n", fd3);return 0;
}
LInux下一切皆文件:
在Linux操作系统中的每一个驱动设备都创建了struct file结构体
结构体内部的属性就是设备的属性数据和函数指针
所以,尽管每一个设备的操作方法不一样,但可以把方法的返回值、参数设置成一样的,然后让这些函数指针指向底层的硬件设备的操作方法,站在Linxu的角度来看这些硬件,也视为文件
实现这种组织的技术其实就是多态
所以对于硬件这一层,在Linux下叫做vfs,即vitural file system虚拟软件系统
5.重定向
5.1什么是重定向
文件描述符的分配规则:查看自己的文件描述表,分配最小的没有被使用的fd
printf和scanf作为C语言的函数接口,同样无法直接和显示器和键盘作交互,其必须依靠相应的文件,也就是标准输出和标准输入
但是依靠的指向并不是FILE*,而是文件描述符,printf依靠1号文件,scanf依靠0号文件
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{close(1);//关掉1号文件(stdout)int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//打开log.txtprintf("Hello Linux\n");//向屏幕打印printf("Hello Linux\n");printf("Hello Linux\n");printf("Hello Linux\n");return 0;
}
可以看到Hello Linux
并没有如愿打印在屏幕上,因为printf只往1号文件里写,就算使用fprintf向stdout输出也不会输出到屏幕
说明printf函数和stdout只认文件描述符1,不管1此时还是不是
标准输出
但1号文件被关闭,系统给新文件log.txt分配最小的没有被使用的fd,也就是1号,所以本来写入到显示器里的内容写入到了log.txt
这就是重定向,重定向的本质:是在内核中改变文件描述符表特定下标的内容
5.2输入重定向和输出重定向
>
代表输出重定向,将输出从显示器更改到指定文件
<
代表输入重定向,将输入从键盘更改到指定文件
测试:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{printf("printf->stdout\n");fprintf(stdout,"fprintf->stdout\n");fprintf(stderr,"fprintf->stderr\n");return 0;
}
直接运行会向显示器打印
重定向后标准输出被重定向到文件中了,但是标准错误仍然还是输出到显示器中
因为
>
是输出重定向,只会更改标准输出stdout的文件描述符,也就是将指向显示器的文件描述符1更改指向log,txt,但是并不会更改标准错误stderr的文件描述符2,所以标准错误依然写入到显示器上,只有标准输出写入到了指定的文件中
>
:将命令的标准输出重定向到文件,会覆盖文件内容。
<
:将文件内容作为命令的标准输入。
>>
:将命令的标准输出重定向到文件,追加到文件末尾,不会覆盖文件内容。
5.3系统调用的重定向dup2
操作系统提供一个专门用于文件描述符改向的系统调用就叫做dup2
oldfd:原始文件描述符,表示要复制的文件描述符。
newfd:目标文件描述符,表示将 oldfd复制给的newfd
返回值:
成功时dup2 返回 newfd(复制操作后新的文件描述符)
失败时返回 -1,并将 errno 设置为相应的错误代码。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd;// 打开文件,如果文件不存在则创建,权限为0644fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);// 将标准输出重定向到fddup2(fd, STDOUT_FILENO);//STDIN_FILENO和STDOUT_FILENO是C语言标准库中定义的宏,分别用于表示标准输入和标准输出的文件描述符,这两个宏在<unistd.h>头文件中定义,通常被赋值为0和1printf("Hello Linux\n");// 关闭原文件描述符close(fd);return 0;
}
6.缓冲区
缓冲区本质上就是一块内存区域
fwrite
等C语言的文件IO接口,都在底层封装了系统调用接口write
,用户调用的所有C语言文件IO接口,都会先写到缓冲区中,然后等到一定条件,在通过一次系统调用,把之前所有缓冲区的数据写入到操作系统中,这个一次性写入过程叫做刷新缓冲区
操作系统也有自己的缓冲区,但是这个是操作系统自己管理的内核缓冲区,其决定了wirte等系统调用接口写入的数据何时写入到内存中,但不是该博客讨论的范围,后续讨论的缓冲区都是用户级缓冲区(语言级别的文件缓冲区)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{close(1);int fd = open("myfile.txt", O_WRONLY|O_CREAT, 0666);printf("fd: %d\n", fd);//fflush(stdout);close(fd);exit(0);//中断程序}
当运行上述代码时,即使对一个文件写入了内容,也没有显示
使用了fflush函数之后,就显示了
原因就是struct FILE 结构体内还有一个语言级别的文件缓冲区
我们使用的printf、fprintf函数等写入的数据都是写到了语言级别的文件缓冲区,而不是到了内存的缓冲区
所以fflush函数所作的工作其实就是把语言级别的缓冲区刷新到内存中
所以如果在文件关闭之前,没有进行刷新,那么,就无法把语言级的缓冲区刷新到内存
用户级缓冲区的刷新策略有以下几种:
1.无缓冲:不进行缓冲,直接输出
2.行缓冲:向显示器写入时,'\n’会强制刷新缓冲区,也就是一行一行刷新缓冲区
3.全缓冲:向普通文件写入时,一般缓冲区被写满才会刷新
4.程序结束,强制刷新缓冲区
5.用户调用flush函数,强制刷新缓冲区
缓冲区被struct FILE管理,由于每个文件的FILE是独立的,因此每个文件的缓冲区也是独立的
特殊情况:
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{fprintf(stdout,"fprintf->stdout\n");char* temp = "write\n";write(1,temp,strlen(temp));fork();return 0;
}
我们成功将内容输出到了显示器
但是,当我们将这些内容重定向到文件中,C语言库的输出打印了两次
为什么write不打印两次?
当我们重定向时,write是系统调用,直接写入struct file的缓冲区(文件内核的缓冲区)
为什么fprintf在fork之前就已经完成了却打印了两次?
fprintf因为是语言层面的调用,将内容写入FILE结构体的缓冲区(语言级别的文件缓冲区),原本向显示器输出的策略是行刷新,但在重定向到log.txt前还没有行刷新将其打印,所以fprint->stdout
就被留在了缓冲区
当子进程被创建时会继承父进程的数据和代码,stdin的FILE指向的缓冲区也会被继承
所以当父子进程同时进行至刷新缓冲区时,分别输出了一句fprint->stdout