文章目录
- 0. 前言
- 1. C文件接口
- 文件打开
- 文件写入
- 2. 系统文件接口
- open && write && close
- open的返回值
本章gitee代码仓库:文件描述符
0. 前言
基础原理知识:
-
文件 = 内容 + 属性
-
文件分为:打开的文件(本章重点讲解)和没打开文件
打开的文件,本质上是进程将其打开
没打开的文件,存储在磁盘
-
要访问这个文件,根据冯诺依曼体系结构,这个文件必须先加载到内存当中,文件又是内容+属性,那么第一步肯定是要先将文件的属性先加载进来,然后根据要不要对这个文件进行读取修改,从而决定是否将内容加载。
-
一个进程可以打开多个文件,操作系统内部一定是存在者大量的被打开的文件,操作系统要对这些文件进行管理,肯定也是先描述,再组织。在内核当中,一个被打开的文件,都必须有自己的打开对象,这里面包含了文件的很多属性。而Linux内核是用C语言写的,所以描述这个文件,用的是
struct
结构体:struct xxx { 文件属性; struct xxx *next; }
这样对文件的管理就变成了对链表的增删查改。
1. C文件接口
文件打开
打开文件的接口fopen
,头文件也是stdio.h
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("pid:%d\n",getpid());
FILE *fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
sleep(1000);
return 0;
}
如果不指定文件路径,则默认在当前工作目录创建或者访问这个文件
这个工作目录就是该进程的工作目录,指令
ll /proc/进程pid
,可查看进程的工作目录,cwd
(current working directory)如果我们修改当前进程的工作目录,则这个创建的文件,则会创建到修改的工作目录里面
chdir("/home/Pyh")
文件写入
这里采用fwrite
作为演示
const char *str = "hello linux\n";
fwrite(str,strlen(str),1,fp);
这里的
strlen(str)
,不需要再加上1('\0'
),因为字符串末尾加'\0'
是C语言的规定,而这个和文件并没有关系,文件只关心这个内容
我们这里发现,这里写入的时候,都会将文件的内容进行清空然后再写入,而>
这个重定向符号,也是会将文件进行清空再写入。
这本质上就因为"w"
操作,会将原始文件的内容进行清空再写入,所以>
打开文件的方式肯定是"w"
方式。
如果想要对文件不进行清空,那么可以采用"a"
操作进行打开a
就就是append
追加,而追加重定向>>
,就是以"a"
方式打开文件。
C语言程序在启动的时候,会默认打开三个输入输出流(文件):
stdin
:标准输入,键盘文件stdout
:标准输出,显示器文件stderr
:标准错误,显示器文件那么我们可以直接向这些文件里面写入,例如:
fwrite(str,strlen(str),1,stdout);
2. 系统文件接口
文件是存储在磁盘上的,而磁盘是外设,所以访问文件就是访问硬件。而硬件是在操作系统之下,是被操作系统管理的,而我们的普通用户,是没有资格去直接访问硬件的,所以需要通过操作系统提供的接口来访问。而这就是系统调用,C语言的库函数,类似printf
、fprintf
、fscanf
、fwrite
这基本上都封装了系统调用。
open && write && close
在系统调用中,我们可以采用open
接口打开文件
int main()
{
umask(0); //修改权限掩码
int fd = open("log.txt",O_WRONLY|O_CREAT,0666); //读取文件|创建,0666设置权限
if(fd < 0) //创建失败返回-1
{
perror("open file error\n");
return 1;
}
return 0;
}
因为涉及到创建文件,所以权限是必须要告诉系统的,不然会出现乱码。
这里我们还修改了权限掩码,具体知识,之前在此篇文章讲到过,有兴趣可查看——Linux权限
O_RONLY
:只读打开O_WRONLY
:只写打开O_RDWR
:读写打开上面这三个必须指定一个且只能指定一个
O_CREAT
:若文件不存在则创建,但必须要使用mode
选项,指定文件访问的权限O_APPEND
:追加写入O_TRUNC
:清空文件内容
有了这些组合,我们就能推断出C语言库函数中的这些"w"
、"a"
封装的是哪些
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd < 0)
{
perror("open file error\n");
return 1;
}
const char *str = "abc";
write(fd,str,strlen(str));
close(fd);
return 0;
}
open的返回值
在操作系统内部描述一个被打开的文件,会直接或者间接包含这些信息:
- 在磁盘的位置
- 文件的基本属性
- 文件的内核缓冲区信息
- 文件结构体指针
struct file *next
在进程的task_struct
结构体中,里面文件结构体指针struct files_struct *files
,它指向了一个文件描述符表struct files_struct
,这个表里面包含一个指针数组struct file *fd_array[]
,它里面存放的就是struct file *
。
当一个文件打开的时候,系统会创建一个文件对象,进程指向的文件描述表里面,就会给它分配一个没有占用的下标来指向这个对象。
所以这个open
的返回值,其实就是返回这个数组的下标。
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
//int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd1 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd2 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd3 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd4 = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd1 < 0)
{
perror("open file error\n");
return 1;
}
const char *str = "abc";
write(fd1,str,strlen(str));
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
close(fd1);
return 0;
}
我们发现这个open
返回的就是这个文件描述符对应数组的下标
但是这里是从3开始的,这就是以为C语言程序会默认打开三个输入输出流文件,这里我们也可以验证一下
C语言
FILE
这个结构体就包含了这个文件描述符
printf("stdin->%d\n",stdin->_fileno);
printf("stdout->%d\n",stdout->_fileno);
printf("stderr->%d\n",stderr->_fileno);
我们发现,这三个输入输出流的文件描述符分别是0、1、2
这里程序启动打开三个输入输出流,并不单是C语言的特性,而是操作系统的特性
因为在计算机开机的时候,我们的屏幕和键盘就被默认打开了,所以我们启动进程的时候,只需要将它们的文件对象地址填到进程的pcb里面
我们再来看一个现象:
close(1);
int n = printf("hello linux\n");
fprintf(stderr,"printf %d\n",n);
我们这里已经关闭的了标准输出,可是这里显示器上还是输出了信息,这是因为每个文件结构体对象中都包含了一个count
引用计数,标准输出和标准错误都是指向的显示器文件,这里的计数就是2
,所以我们close(1)
其实是将该文件计数减一,然后再将1
号位置置空,如果这个这个count
计数不为零,则不管它,如果为零了,则系统回收这个文件对象。