基础IO
文件相关知识补充
当我们在磁盘上创建了一个空文件的时候,空文件是否会占据空间呢?
答案是会占据空间,当我们创建一个文件,包括文件的名字,创建时间,大小等。这些称作文件的属性。所以 文件 = 内容 + 属性。
-
所有对文件的操作分为两部分 A:对文件内容的操作 B:对文件属性的操作
-
文件的内容是数据,文件的属性也是一种特殊的属性,所以存储文件时既要存储内容也要存储属性数据。
-
进程要访问一个文件的时候,要先把这个文件打开,文件打开前是普通的磁盘文件,将文件加载到内存当中后文件才算被打开。文件按照是否被打开,分为被打开的文件,没有被打开的文件。
一个进程通过操作系统可以打开多个文件吗
打开到文件就是加载到内存中,一个进程可以打开多个文件。
结论
因为文件默认是在磁盘上的,所以将磁盘上的文件加载到内存当中,一定会涉及到访问磁盘设备需要操作系统来做,操作系统在运行的过程中可能会打开多个文件,那么操作系统也需要对打开的文件进行管理(先描述,在组织)
一个文件要被打开,一定要在内核中形成被打开的文件对象。
C语言文件接口
fopen
作用
打开文件
头文件
#include<stdio.h>
函数参数及返回值
FILE* fopen(const char *path, const char *mode);
参数
path要打开文件的路径
mode打开文件的模式
- w方式:打开文件会先清空文件内容 (echo >)
- r方式:只以读方式打开文件
- a方式:从文件结尾处开始写,追加不清空文件(echo >>)
- r+方式:以读和写的方式打开文件
返回值
成功时返回文件指针,失败时返回NULL
fclose
作用
关闭一个文件流
函数参数以及返回值
int fclose(FILE* stream)
返回值:
成功时返回0,失败时返回
EOF
fread
作用
从文件中读取内容
函数参数以及返回值
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数
指定的文件流
stream
中读取数据到ptr
指向的数组中。
size
是每个数据项的大小(以字节为单位),
nmemb
是要读取的数据项的数量。返回值
成功时返回实际读取的数据项数(不是字节数)
fwrite
作用
向文件中写入内容
函数参数以及返回值
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数
将数据写入到指定的文件流
stream
中。
ptr
指向要写入的数据
size
是每个数据项的大小(以字节为单位)
nmemb
是要写入的数据项的数量。返回值
- 成功时返回实际写入的数据项数(不是字节数)。
代码示例
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
return 1;
}
char* message = "hello world\n";
size_t n = fwrite(message,1,strlen(message),fp);
printf("写入文件的数据项的数量:%zu\n",n);
fclose(fp);
return 0;
}
运行结果
系统文件I/O
系统调用相关接口
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。我们都知道一个进程想要打开一个文件需要操作系统的介入,操作系统为打开文件提供系统调用接口。
我们学习的C语言打开文件的接口,底层一定是封装了系统调用接口。
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);
参数
flags 参数是通过命令文件访问模式与其他可选模式相结合的方式来指定的,open 调用必须指定以下文件访问模式之一:
1)
O_RDONLY
:以只读方式打开;2)
O_WRONLY
:以只写方式打开;3)
O_RDWR
:以读写方式打开。4)
O_APPEND
:把写入数据追加在文件的末尾;5)
O_TRUNC
:把文件长度设置为零,丢弃已有的内容这些模式用 | (按位或运算符)
pathname:要打开文件的路径
mode:如果文件不存在创建文件时赋给文件的权限
返回值
如果调用成功,它将返回一个可以被
read、write
和其他系统调用使用的文件描述符。这个文件描述符是唯一的,不会与任何其他运行中的进程共享。在调用失败时,将返回 -1 并设置全局变量errno
来指明失败的原因。
下面我们举个例子来讲解flags参数的原理
#include<stdio.h>
#define Print1 1 // 0001
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000
void Print(int flags)
{
if(flags & Print1)
{
printf("hello 1\n");
}
if(flags & Print2)
{
printf("hello 2\n");
}
if(flags & Print3)
{
printf("hello 3\n");
}
if(flags & Print4)
{
printf("hello 4\n");
}
}
int main()
{
Print(Print2);
Print(Print1 | Print2 | Print3);
return 0;
}
按位或运算符是有一个为1就为1,那么通过按位或运算,得到一串二进制,最后判断哪个位上为1就证明用了此方式。
close
作用
关闭一个打开的文件
头文件
#include<unistd.h>
函数参数以及返回值
int close(int fildes);
参数
fildes
:要关闭文件的文件描述符返回值
当 close 系统调用成功时,返回 0,文件描述符被释放并能够重新使用;调用出错,则返回 -1
write
作用
向一个指定的文件标识符中写入数据
头文件
#include<unistd.h>
函数参数及其返回值
ssize_t write(int fd, const void *buf, size_t count)
系统调用 write 的作用是把缓冲区
buf
的前nbytes
个字节写入与文件描述符fildes
关联的文件中。它返回实际写入的字节数。如果文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能会小于nbytes
。如果函数返回值为 0,就表示没有写入任何数据;如果返回值为 -1,则表明write
系统调用出现了错误,错误代码保存在全局变量errno
里。
注:
count要不要 + 1就是\0加入进去
- 当我们想像文件中写入字符串的时候,不需要strlen + 1,\0是C语言的规定,不是文件的规定。
向有内容的文件中写入的时候是覆盖式的写入,如果要清空源文件中内容需要使用O_TRUNC
选项
read
作用
从某个文件描述符中读取内容
函数头文件
#include<unistd.h>
函数参数及返回值
size_t read(int fildes,void *buf,size_t nbytes);
参数
fildes
:文件描述符buf
:把文件读到的缓冲区nbytes
:从文件中读出的字节数返回值:
实际读到的字节数这可能会小于请求的字节数
C语言函数和 系统调用接口之间的关系
在C语言中,常用的文件操作函数包括
fopen、fclose、fread、fwrite、fseek
等。这些函数分别用于打开文件、关闭文件、从文件中读取数据、向文件中写入数据以及定位文件中的读写位置等操作。以
fopen
函数为例,它用于打开一个文件并返回一个指向该文件的FILE指针。在内部实现中,fopen
函数会调用操作系统的open系统调用来打开文件,并处理相关的错误和异常情况。开发者在使用fopen
函数时,无需关心底层的系统调用细节,只需按照函数接口说明传递参数即可。
文件描述符fd
从规律上来看我们发现fd
是一个连续的小整数
文件在操作系统中的表现
每次操作系统运行代码时会为这个进程创建一个task_struct
,进程打开文件的时候,相当于操作系统打开了很多文件,操作系统对打开的文件要创建对应的结构体来方便管理,这个结构体就叫做 struct file
,这个就是一个被打开文件的描述结构体对象。
但是在操作系统中存在很多进程和很多的被打开的文件的描述结构体对象,那么一个进程怎么找到这个进程对应的打开的文件呢?
在操作系统内核中为进程设计了一个结构体叫做,
struct files_struct
,这个结构体中包含了一个数组,类型为struct file *fd array[]
。当打开一个文件的时候 会帮我们创建一个
struct file
对象,让后把文件对象的地址填入到数组中,然后把数组的下标返回给上层,我们就把这个数组的下标叫做文件描述符。把这个数组叫做进程文件描述表。
文件描述符的本质就是数组的下标!!!
那么对应的C语言的接口中的FILE其实就是一个C语言提供的结构体类型,里面必定封装了文件描述符!!!
验证代码
int main()
{
//打开文件
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("open");
return -1;
}
printf("log.txt的文件描述符 %d\n",fp->_fileno);
printf("stdin的文件描述符 %d\n",stdin->_fileno);
printf("stdout的文件描述符 %d\n",stdout->_fileno);
printf("stderr的文件描述符 %d\n",stderr->_fileno);
//关闭文件
fclose(fp);
}
文件描述符的0,1,2
进程在运行的时候默认是把,标准输入(键盘),标准输出(显示器),标准错误(显示器)。在C语言中标准输入对应的是stdin
,在系统中对应的为0,标准输出对应的是stdout
,在系统中对应的是1,标准错误对应的是stderr
,在系统中对应的是2。
标准错误
在Linux中,标准错误(stderr
)是计算机程序与其环境之间预连接的一个重要的输出通信通道。它是程序用于输出错误消息、警告或其他诊断信息的输出流。以下是关于Linux中标准错误的详细解释:
定义与特点
- 定义:标准错误是程序在运行时遇到错误、警告或其他非正常情况时,用于向用户报告这些信息的一个独立的输出流。
- 特点:
- 它是独立于标准输出(
stdout
)的,可以单独进行重定向。 - 默认情况下,标准错误和标准输出都被连接到屏幕(通常是终端或控制台),但用户可以通过重定向操作来改变它们的目的地。
- 它是独立于标准输出(
2>&1
把文件标识符1里面的内容覆盖到文件表示符2里
操作系统为什么默认要把stdin
,stdout
,stderr
打开呢
为了让程序员默认进行输入输出代码编写!!!
我们都听说过Linux下一切皆文件,如何理解呢
键盘,显示器,磁盘,网卡都有自己的读,写方法。读写方法都是不同的,但是当我们对文件操作的系统调用是只有一套的,这是因为在上层存在
struct file
结构体里面会存放着两个函数指针一个是int(*read)一个是int ( *write )这两个函数指针指向的就是底层的自己的读写方法。这一层就是虚拟文件系统简称VFS
。
fd
的分配规则
进程默认已经打开了0,1,2我们可以直接使用0,1,2进行数据的访问
代码验证
nt main()
{
//打开文件
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
if(fd < 0)
{
perror("open");
return -1;
}
printf("log.txt 的文件描述符为:%d\n",fd);
//关闭文件
close(fd);
}
文件描述符的分配规则是,寻找最小的,没有被使用的数据的位置,分配给指定的打开文件。
代码验证
int main()
{
//关闭标准输入
close(0);
//打开文件
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
if(fd < 0)
{
perror("open");
return -1;
}
printf("log.txt 的文件描述符为:%d\n",fd);
//关闭文件
close(fd);
}
struct file
内核对象
在Linux文件系统中,struct file
是内核中表示打开文件的一个核心数据结构。这个结构体包含了关于一个打开文件的所有信息,包括文件的状态(如当前读写位置、打开模式、文件状态标志等)、文件操作的方法(通过指向 struct file_operations
的指针实现)、以及与文件关联的底层文件系统或设备的信息。
当用户在用户空间打开一个文件时,内核会创建一个 struct file
实例来表示这个打开的文件。这个实例包含了从用户空间程序到内核中文件系统的所有必要链接和状态信息。
struct file
的一些关键成员包括:
f_path
或f_dentry
和f_vfsmnt
:这些成员包含了关于文件在文件系统中的位置的信息,包括文件的dentry
(目录项)和文件所在的vfsmount
(挂载点信息)。f_op
:指向struct file_operations
的指针,后者包含了对该文件进行操作的函数指针集合,如读、写、打开、关闭等。f_lock
:用于保护文件结构的自旋锁,确保在多线程或多进程环境中对文件的访问是安全的。f_count
:表示文件被打开的次数。只有当f_count
变为0时,文件才会被真正关闭,并且相关的struct file
实例和底层资源才会被释放。f_flags
和f_mode
:表示文件的打开标志和模式(如只读、只写、读写)。f_pos
:文件的当前读写位置。f_owner
、f_uid
和f_gid
:与文件打开相关的用户和组信息。private_data
:一个指向任意数据的指针,通常由文件系统或设备驱动使用,以存储与特定文件实例相关的私有数据。
struct file
结构体对象是在内核中创建,专门用来管理被打开文件
struct file
结构体中存在着一个文件缓冲区,上层想读文件,前提必须要把磁盘中的文件的数据加载到文件缓冲区中。所以读数据要先把数据加载到内存当写数据的时候要讲数据先加载到文件缓冲区当中,然后最后换入到磁盘当中
所以我们在应用层进行数据的读写的本质就是将内核缓冲区中的数据,进行来回的拷贝。
重定向
dup2
我们可以不需要像上面一样先关闭一个再打开一个进行重定向,我们可以使用文件描述符表级别的数组的内容的拷贝。
我们可以使用dup2
接口
函数参数及返回值
int dup2(int oldfd, int newfd)
- 最后
oldfd
会覆盖newfd
,oldfd
保留下来了- 此时
newfd
,oldfd
指向相同的struct file
,为了保证多个fd
指向同一个struct file
,引入了引用计数,在stuct file
中存在f_count
。
输出重定向
代码
int main()
{
//打开文件
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC ,0666);
if(fd < 0)
{
perror("open");
return -1;
}
dup2(fd,1);//输出重定向
printf("hello file!\n");
//关闭文件
close(fd);
}
追加重定向
代码
int main()
{
//打开文件
int fd = open("log.txt",O_CREAT | O_WRONLY | O_APPEND ,0666);
if(fd < 0)
{
perror("open");
return -1;
}
dup2(fd,1);//追加重定向
printf("hello file!\n");
//关闭文件
close(fd);
}
输入重定向
代码
int main()
{
//打开文件
int fd = open("log.txt", O_RDONLY );
if(fd < 0)
{
perror("open");
return -1;
}
dup2(fd,0);//输入重定向
char buffer[1024];
read(stdin->_fileno,buffer,1024);
printf("%s\n",buffer);
//关闭文件
close(fd);
}
总结
重定向的本质,其实就是修改特定文件
fd
的下标内容重定向原理
上层
fd
不变,底层fd
指向的内容在改变
程序替换与重定向
程序替换不会影响曾经的重定向
为什么
程序替换只会替换代码和数据,对地址空间和PCB,文件描述符表是不受影响的。
本专栏为“小菜”linux学习之路
该文章仅供学习参考,如有问题,欢迎在评论区指出。