文件描述符(上)
一、预备知识
在系统角度理解文件:
- 文件包括:文件内容 + 文件属性
- 对文件的所有操作也无外乎是对内容和属性的操作
如何访问文件?
- 访问文件本质是进程在向文件进行读写操作
- 文件在磁盘(硬件)上存放,要向硬件进行读取和写入操作必须通过中间层操作系统,也就是说操作系统必须提供文件相关的系统调用接口
- 不同的语言有不同语言级别的文件访问函数,但是底层封装的都是文件类系统调用接口。
有了系统调用,为什么各种语言还要设计自己的文件类接口?
- 系统调用的使用难度较大,对开发人员的要求较高。语言上对这些接口进行封装,是为了让文件操作更简单。
- 为了实现语言的跨平台性:不同的操作系统有不同的文件类系统调用,如果不进行封装而直接使用系统调用会使编写的代码无法在其他平台运行,不具备跨平台性!
提示:为了实现语言的跨平台性,语言开发人员会将所有平台的代码都实现一遍。在实际应用时在通过条件编译动态裁剪代码。
Linux下,一切皆文件!
-
狭义上(软件层面):文件就是普通的磁盘文件
-
广义上(系统层面):只要能被input或output的设备就叫做文件!显示器,键盘,网卡,磁盘等等,几乎所有的外设都可以看做文件。
二、C语言文件类函数
2.1 fopen & fclose
参数:
-
path:文件的所在路径,相对路径或绝对路径
-
mode:文件的打开方式:
返回值:
- 打开成功返回文件指针(FILE*);
- 打开失败返回NULL,并设置errno
参数:
- 待关闭文件的文件指针
返回值:
- 成功返回0;
- 失败返回EOF,并设置errno
注意:fcolse内部封装fflush,在关闭文件前会先将缓冲区中的数据刷新。
解释:
为什么可以通过相对路径找到文件位置?
-
所谓的相对路径,就是相对进程的当前工作路径。
-
进程会记录自己的当前工作路径cwd(current working directory),在/proc目录中可以查看进程的cwd文件。
-
进程的工作路径是指进程在执行文件操作时的默认路径。它可以是程序所在的路径,但也可以是其他路径。
-
当一个程序被执行时,操作系统会为该程序创建一个进程,并为该进程分配一个工作路径。这个工作路径通常是启动程序时所在的目录,也就是程序所在的路径。但是,进程的工作路径可以在运行时被修改,例如通过调用系统函数chdir来改变工作路径。
2.2 fputs
写入文件测试:
#include <stdio.h>
#include <string.h>
int main(){
//打开文件
FILE *fp = fopen("./myfile", "w"); //使用相对路径:./表示当前目录,../表示上一级目录
if(fp == NULL) //打开失败返回NULL
{
perror("fopen");
return 1;
}
//写入文件操作
char *s1 = "hello fputs!\n";
fputs(s1, fp);
char *s2 = "hello fprintf!\n";
fprintf(fp, "%s", s2);
char *s3 = "hello fwrite!\n";
fwrite(s3, strlen(s3), 1, fp);
//关闭文件
fclose(fp);
return 0;
}
运行结果:
解释:
-
w(write)模式下,会先将文件内容清空再写入新内容,即使不进行写入也会清空内容。如果文件不存在会创建新文件,实际上是进程通过系统调用创建的。
-
a(append)模式下,会从文件末尾追加新内容,如果文件不存在会创建新文件。
-
fwrite写入的字符串大小要不要+1,或者说要不要将’\0’也写入文件?不需要!'\0’结尾是C语言的规定,文件不需要遵守。文件内要保存的是有效数据。
-
C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符。
小技巧:
echo "hello world!" > log.txt
输出重定向,向文件写入,写入前先清空文件内容。> log.txt
清空文件内容
2.3 fgets
读取文件测试:编写一个cat程序,用于读取并打印其他文件的内容
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[]){
if(argc != 2)
{
printf("argc error!\n");
return 1;
}
//打开文件
FILE *fp = fopen(argv[1], "r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
//读取文件操作
char buffer[64];
while(fgets(buffer, sizeof(buffer), fp))
{
fprintf(stdout, "%s", buffer);
}
//关闭文件
fclose(fp);
return 0;
}
运行结果:
解释:
-
fgets 从指定的流 stream 读取一行
-
C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
-
fgets会自动在读取到的字符串结尾添加’\0’
-
-
fprintf 发送格式化输出到流 stream 中
-
C 库函数 int fprintf(FILE *stream, const char *format, …) 发送格式化输出到流 stream 中。
-
执行一个 Shell 命令行时通常会自动打开三个标准文件,即标准输入文件(stdin),通常对应终端的键盘;标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。
-
Linux下,一切皆文件!
-
-
fgets/fputs是专门用于向文件输入输出字符串的函数,因此他们会考虑’\0’和’\n’的问题。而fread/fwrite只是读取和写入指定大小的内存块。
三、文件类系统调用
3.1 open && close
参数:
-
pathname:文件的所在路径,相对路径或绝对路径
-
flags:打开模式标志位(位图),必须包括宏O_WRONLY,O_RDONLY,O_RDWR三者之一
- O_WRONLY:以只写模式打开文件
- O_RDONLY:以只读模式打开文件
- O_RDWR:以读和写模式打开文件
- O_CREAT:如果文件不存在,则创建文件
- O_TRUNC:打开已存在的普通文件时,如果文件允许写入,就清空原内容
- O_APPEND:以追加模式打开文件,会从文件末尾追加新内容
-
mode:创建文件时,设置新文件的文件权限,使用8进程权限码。受系统umask的影响,可以通过umask系统调用设置属于进程的权限掩码。
返回值:
- 打开成功,返回打开文件的文件描述符
- 打开失败,返回-1,并设置errno
参数:
- fd:要关闭文件的文件描述符
返回值:
- 关闭成功,返回0
- 关闭失败,返回-1,并设置errno
注意:close不会刷新缓冲区中的数据,需要在close前调用fflush手动刷新。
用系统调用open创建并打开文件,然后返回文件描述符,最后关闭文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
umask(0); //设置属于该进程的权限掩码
int fd = open("./myfile", O_WRONLY | O_CREAT, 0666); //创建并以只写方式打开myfile,设置文件权限为0666
if(fd == -1) //打开失败
{
perror("open");
return 1;
}
printf("open success! file descriptor:%d\n", fd); //打开成功,返回文件描述符
close(fd);
return 0;
}
运行结果:
解释:
- 用int中一个不重复的比特位,就可以标识一种状态:
#include <stdio.h>
#include <string.h>
enum FLAGS{ //每个选项只有一个比特位为1
ONE = 1,
TWO = 2,
THREE = 4
};
void show(int flags){
if(flags & ONE) printf("task one!\n"); //判断位
if(flags & TWO) printf("task two!\n");
if(flags & THREE) printf("task three!\n");
}
int main(){
show(ONE);
printf("----------------------\n");
show(ONE | TWO); //打开位
printf("----------------------\n");
show(ONE | TWO | THREE);
}
运行结果:
- 系统调用umask:设置属于该进程的权限掩码
3.2 write
参数:
- fd:被写入文件的文件描述符;
- buf:待写入的缓冲区;
- count:要写入的字节数;
注意:
要想进程write写入操作,open必须设置O_WRONLY。
open如果只有O_WRONLY,write默认是覆写模式,即不对原文件内容做清空,直接覆盖式写入。
在open打开文件时加入O_TRUNC标志位,会清空原文件内容。
在open打开文件时加入O_APPEND标志位,会从原文件末尾追加内容。
测试代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
umask(0);
int fd = open("./myfile", O_WRONLY | O_CREAT, 0666);//默认覆写
//int fd = open("./myfile", O_WRONLY | O_CREAT | O_TRUNC, 0666);//写前清空,C语言fopen(path,"w")的底层调用;
//int fd = open("./myfile", O_WRONLY | O_CREAT | O_APPEND, 0666);//追加模式,C语言fopen(path,"a")的底层调用;
if(fd == -1)
{
perror("open");
return 1;
}
printf("open success! file descriptor:%d\n", fd);
//进行写入操作
char *str = "aaa\n";
write(fd, str, strlen(str));
//关闭文件
close(fd);
return 0;
}
运行结果:
3.3 read
参数:
- fd:被读取文件的文件描述符;
- buf:将读取到的内容填写到缓冲区;
- count:要读取的字节数;
注意:
- 要想进行read读取操作,open必须设置O_RDONLY。
- 需要手动在读取到的字符串末尾添加’\0’,表示字符串结束。
测试代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
umask(0);
int fd = open("./myfile", O_RDONLY); //以只读模式打开文件
if(fd == -1)
{
perror("open");
return 1;
}
printf("open success! file descriptor:%d\n", fd);
//进行读取操作
char buffer[64];
memset(buffer,'\0', sizeof(buffer)); //手动加'\0'
read(fd, buffer, sizeof(buffer)-1); //留一个位置给'\0'
fprintf(stdout, "%s", buffer);
//关闭文件
close(fd);
return 0;
}
运行结果:成功读取到上一次追加测试的文件内容
四、文件描述符
4.1 文件描述符0,1,2
测试代码:
int main(){
int fd1 = open("./myfile1", O_WRONLY | O_CREAT);
int fd2 = open("./myfile2", O_WRONLY | O_CREAT);
int fd3 = open("./myfile3", O_WRONLY | O_CREAT);
int fd4 = open("./myfile4", O_WRONLY | O_CREAT);
printf("open success! fd1:%d\n", fd1);
printf("open success! fd1:%d\n", fd2);
printf("open success! fd1:%d\n", fd3);
printf("open success! fd1:%d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
运行结果:
从以上的测试我们可以看出,文件描述符是从3开始的连续整数。那么0,1,2,这三个文件描述符分别代表什么文件呢?
还记的我们曾经说过程序运行时会自动打开三个标准文件:stdin, stdout, stderr。没错文件描述符0,1,2就依次对应他们三个。
我们来测试证明一下:
int main(){
//stdin -> 0
char buffer[64];
//fscanf(stdin,"%s", buffer); //'\n'不会被读取,会自动在末尾加'\0'
//printf("%s\n", buffer);
ssize_t s = read(0, buffer, sizeof(buffer)-1); //'\n'会被读取,不会自动在末尾加'\0'
buffer[s] = '\0';
printf("%s", buffer);
//stdout -> 1
const char* str1 = "stdout!\n";
const char* str2 = "stderr!\n";
//fprintf(stdout, str1);
write(1, str1, strlen(str1));
//stderr -> 2
//fprintf(stderr, str2);
write(2, str2, strlen(str2));
}
运行结果:使用文件指针和文件描述符两种方案进行输入输出结果都一样!
结论:
- 标准输入流文件:文件指针(FILE*)stdin,文件描述符(int)0;
- 标准输出流文件:文件指针(FILE*)stdout,文件描述符(int)1;
- 标准错误流文件:文件指针(FILE*)stderr,文件描述符(int)2;
4.2 FILE是什么类型?
- FILE是C标准库提供的一种struct结构体,内部封装了文件描述符。
- FILE结构体成员_fileno即文件描述符。
4.3 文件描述符fd是什么?
- 进程要访问文件,前提是先将文件加载到内存中(内存文件),才能直接访问。
- 为了方便管理系统中(内存中)大量被打开的文件,操作系统需要将这些文件先描述,再组织!
- 先描述:系统内部为每一个被打开的文件都创建一个file结构体(也称文件对象),当中包含了关于该文件的所有内容,属性(inode结构)和方法(函数指针)。
- 再组织:在进程的内核数据结构files_struct中有一个文件指针数组(文件描述符表)fd_array,专门用于存放该进程所有打开文件的file结构体指针
- 文件描述符fd本质是文件描述符表fd_array的一个下标。
- 进程通过文件描述符fd与文件对象建立联系,再通过文件对象操作对应的文件。
- 当同一个文件被多个进程打开时,该文件只需要向内存加载一次,创建一个file结构体即可。之后每当一个进程打开该文件时,直接将文件对象指针填写到该进程的文件描述符表中即可。
文件分为:
- 内存文件:被进程打开的文件
- 磁盘文件:没有被打开的文件
fopen,fwrite的具体工作流程:
- fopen --> open --> fd --> FILE --> return FILE*
- fwrite() --> FILE* --> fd --> write(fd, …) --> task_struct --> *fs --> files_struct --> fd_array[fd] --> struct file -->进行写入操作