C语言(标准库IO接口)
打开、关闭文件
- path:文件路径名
- mode
r:只读
r+:可读可写
w:只写,文件不存在则创建,若文件存在则清空原有内容
w+:相较于w,多出了可读操作
a:追加写打开,若文件不存在则创建,每次总是写到文件末尾
a+:相较于a,多出了可读操作 - 返回值:文件的操作句柄(文件流指针)
- fp:文件的操作句柄
读写文件
- ptr:用于接收数据的缓冲区
- size:一次要读取的字节大小
- nmemb:一次要读取多少个 size
- stream:fopen返回的文件流指针
- 返回值:成功读取到的字节大小 若读取到文件末尾:返回0
- ptr:要写入的数据
- size:一次要写入的字节大小
- nmemb:一次要写入多少个 size
- stream:fopen返回的文件流指针
- 返回值:实际写入的块个数
文件指针位置重定位
- 对文件的读写位置跳转到whence位置偏移offset个字节处
- stream:文件的操作句柄
- whence:跳转的起始位置
SEEK_SET:文件起始位置
SEEK_CUR:当前读写位置
SEEK_END:文件末尾位置 - offset:偏移量
测试用例
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("./myfile.txt", "r+");
if(!fp)
{
printf("fopen error\n");
return -1;
}
const char* str = "hello world\n";
size_t wnum = fwrite(str, 1, strlen(str), fp);
if(wnum == 0)
{
printf("fwrite error\n");
return -1;
}
printf("wnum:%d\n", (int)wnum);
//跳转到文件的起始位置,否则会读取不到数据,因为此时的文件指针在末尾
fseek(fp, 0, SEEK_SET);
char buf[1024] = {0};
size_t rnum = fread(buf, 1, sizeof(buf) - 1, fp);
printf("rnum:%d --- buf:%s\n", (int)rnum, buf);
//关闭文件
fclose(fp);
return 0;
}
Linux(系统调用IO接口)
打开、关闭文件
- pathname:文件路径名
- flags:选项参数,文件的打开方式,必选项、可选项
必选项(只能选择其一):O_RDONLY—只读 O_WRONLY—只写 O_RDWR—读写
可选项:
O_CREAT:若文件存在则打开,否则创建新文件
O_EXCL:与O_CREAT同时使用,若文件存在则报错,不存在则创建新文件
O_TRUNC:打开文件的同时清空原有内容
O_APPEND:追加写,总是将数据写入到文件末尾 - mode:文件的权限,如果使用了O_CREAT有可能创建新文件,就一定要指定文件权限,八进制数字形式
- 返回值:一个非负整数,文件描述符,文件的操作句柄 错误:-1
fd:文件的操作句柄
读写文件
- fd:文件的操作句柄
- buf:要写入文件的数据的空间首地址
- count:要写入的数据大小
- 返回值:返回实际写入文件的数据字节长度 错误:-1
- fd:文件的操作句柄
- buf:从文件中读取数据放到哪块缓冲区中的首地址
- len:想要读取的数据长度,注意这个len不能大于缓冲区的大小
- 返回值:返回的是实际读取到的数据字节长度 错误:-1
文件指针位置重定位
- fd:文件的操作句柄
- offset:偏移量
- whence:从哪里开始偏移
SEEK_SET:文件的起始位置
SEEK_CUR:文件当前读写位置
SEEK_END:文件末尾 - 返回值:成功返回当前位置相对于起始位置的偏移量 失败:-1
测试用例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
int main()
{
umask(0);
int fd = open("./myfile.txt", O_RDWR | O_CREAT, 0777);
if(fd < 0)
{
perror("open error");
return -1;
}
//每次将写位置跳转到文件末尾,相当于追加写
lseek(fd, 0, SEEK_END);
const char* str = "hello world\n";
ssize_t ret = write(fd, str, strlen(str));
if(ret < 0)
{
perror("write error");
return -1;
}
printf("ret:%d\n", (int)ret);
//跳转到文件的起始位置,否则会读取不到数据,因为此时文件指针在末尾
lseek(fd, 0, SEEK_SET);
char buf[1024] = {0};
ret = read(fd, buf, sizeof(buf) - 1);
if(ret < 0)
{
perror("read error");
return -1;
}
printf("ret:%d---buf:%s\n", (int)ret, buf);
//关闭文件
close(fd);
return 0;
}
文件描述符
文件描述符:非负整数(内核中文件描述信息结构体数组的下标)
进程通过 struct file 结构体来描述打开的文件,使用了 struct file *fd_array[]; 文件描述符就是这个数组的下标;用户打开文件,操作系统通过 struct file 结构体描述文件,并且将指针添加进 fd_array 数组中,向用户返回这个文件描述信息在数组中位置(下标),用户操作文件的时候,将这个下标传递给操作系统,操作系统通过下标找到文件描述信息进而操作文件。
为什么打开一个文件后,如果不操作了一定要关闭,释放资源?
文件描述符实际是有限的,若不关闭文件,文件描述符用完了后,则在进程中就打不开新文件了。
文件描述符的分配规则:最小未使用
一个程序运行起来后,进程中默认打开三个文件:标准输入stdin(0),标准输出stdout(1),标准错误stderr(2)
标准输入 | 标准输出 | 标准错误 | |
---|---|---|---|
0 | 1 | 2 | 文件描述符(int) |
stdin | stdout | stderr | 文件流指针(FILE*) |
测试用例:
//这个demo体会关闭1号文件描述符,即标准输出
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main()
{
//将文件权限掩码设置为0
umask(0);
//关闭标准输出文件描述符
close(1);
int fd = open("./test.txt", O_RDWR | O_CREAT, 0664);
if(fd < 0)
{
perror("open error");
return -1;
}
printf("fd = %d\n", fd);
fflush(stdout);
//关闭文件,释放资源
close(fd);
return 0;
}
从上述例子我们可以发现 printf 并非真的一定要把数据写入标准输出文件,而是因为 printf 函数中操作文件的时候操作的描述符是1。原本 向1中写入数据就是向标准输出写入,然后当1指向了 新的文件后,这时候 printf 就会将数据写入到指定的新的文件中。
重定向
将数据不再写入原本的文件,而是写入新的指定的文件中,实现方式就是替换这个描述符对应的文件描述信息。实际是描述符的重定向,改变描述符所指向的文件,就改变了数据的流向。
描述符重定向函数:
让 newfd 这个描述符也指向 oldfd 所指向的文件,这时候 oldfd 和 newfd 都能够操作 oldfd 所指向的文件。
测试用例:
//这个demo体会关闭1号文件描述符,即标准输出
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main()
{
//将文件权限掩码设置为0
umask(0);
int fd = open("./test_dup2.txt", O_RDWR | O_CREAT, 0664);
if(fd < 0)
{
perror("open error");
return -1;
}
//将1号文件描述符也指向fd所描述的文件
dup2(fd, 1);
printf("fd = %d\n", fd);
fflush(stdout);
//关闭文件,释放资源
close(fd);
return 0;
}
文件描述符与文件流指针的关系
- 库函数的操作句柄是文件流指针
- 系统调用接口的操作句柄是文件描述符
库函数是对系统调用接口的一层封装,通过文件流指针进行最终文件操作的时候,依然还要能够找到文件对应的文件描述符才可以 。文件流指针是一个结构体,结构体中有很多成员变量,其中就有一个叫 _fileno ,这就是文件描述符。
库函数接口向文件中写入数据,并不会直接写入文件,而是先写入缓冲区中,刷新缓冲区的时候才会写入文件。
系统调用接口是直接将数据写入文件的,系统调用接口没有这个缓冲区的。只有库函数才存在这个缓冲区。
测试用例:
//这个demo体会库函数接口和系统调用接口 缓冲区的有无及输出
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//库函数接口必须刷新缓冲区后才会将数据打印,程序退出前会刷新缓冲区,所以最后打印
fprintf(stdout, "%s---%d", "fprintf", 1);
fwrite("fwrite", 1, 6, stdout);
printf("printf");
//系统调用接口直接将数据写入到文件中,所以先打印
write(1, "write", 5);
sleep(3);
return 0;
}
文件系统
Linux下的 ext2 文件系统为例。文件系统就是磁盘上管理文件的系统。
存储一个文件需要找到空闲的磁盘块存储文件数据,以及需要找到一个未被使用的 inode 节点存储自己的元信息。
针对每一个磁盘块做一个整体的位图,这样就可以快速找到空闲的磁盘块。
文件的权限就是使用位图存储,一个用户没有权限也就是 0/1 的关系,位图可以节省大量的空间以及二进制的与或非速度非常快。
文件的存储过程:通过inode_bitmap在inode区域获取空闲inode节点,通过data_bitmap获取空闲数据块,在inode结点中记录文件信息以及数据块位置,并且将文件数据写入到数据块中,将自己的目录项信息(inode节点号+文件名)添加到所在目录文件中。
目录文件:一个文件–>文件中记录的目录下的文件信息(文件名+inode节点号)–>目录项
文件的查找过程:通过目录项信息快速找到文件的inode节点号,通过inode节点号可以在inode table中快速找到文件的inode节点;通过inode节点可以找到文件数据存储位置,进而获取到数据。
软链接、硬链接文件
给一个源文件创建一个软链接文件/硬链接文件,就可以通过被创建出来的软链接文件/硬链接文件来操作源文件
为源文件创建一个硬链接文件:ln failname.txt failname.hard
为源文件创建一个软链接文件:ln -s failname.txt failname.soft
软链接文件和硬链接文件的区别 :
硬链接文件:本质上和源文件没有什么不同,都是一个文件的名称,与源文件共用同一个 inode节点,通过自己的 inode节点访问源文件数据。
软链接文件:本质上是一个独立的文件,有自己的 inode节点,文件数据中保存源文件的路径,通过这个路径访问源文件的数据。
删除上的区别:
删除源文件,软链接失效,硬链接文件只是链接数 -1,链接数(一个 inode节点对应有几个目录项)
删除一个文件,文件并不会立即被删除,而是直接删除了目录项信息,inode中的链接数 -1,此后链接数为 0 时,才会真正删除文件
软链接文件可以跨分区,硬链接文件不可以
软链接文件可以对目录创建,硬链接不可以