标准C库IO函数工作流程
用 fopen
打开文件,返回类型为 FILE *
,其是一个结构体,包含
- 文件描述符(整型值)——索引到对应的磁盘文件
- 文件读写指针位置——读写文件过程中指针的实际位置
- I/O缓冲区(内存地址)——通过寻址到对应的内存块
读写文件不是直接写入或读磁盘,而是先写入内存中的缓冲区,待缓冲区满了才放入磁盘中。
IO缓冲区的作用?
- 大部分硬盘都是机械硬盘,读取寻道时间和写入寻道时间都是在毫秒级ms;
- 相对来说,内存读写速度都非常块,因为内存属于电子设备,读写速度是纳秒级ns;
- 两者之间的读写速度相差一百万倍;
而将缓冲区的数据刷新到磁盘有三种方式
fflush
刷新缓冲区- 缓冲区满了
- 正常关闭文件,包括
fclose
、return(main函数)
、exit (main函数)
标准C库函数与Linux系统函数之间的关系
C标准函数调用Linux系统的API,然后调用内核层设备驱动函数通过设备驱动操作硬件。
虚拟地址空间
内核区存在PCB进程控制块,其包含文件描述符表,0~2是默认打开的,之后每打开一个新文件,则占用一个文件描述符,而且使用的是空间的最小的一个文件描述符。
- 环境变量(env)——存放进程需要的环境变量
- 命令行参数——由
char* argv[]
获取 - 栈空间——存放局部变量等,地址空间使用是从上往下的
- 共享库——存放C标准库,Linux系统IO函数,动态库最后也是加载到共享库中
- 堆空间——
malloc
申请的空间就存放在这 - .bss(未初始化全局变量)—— 为0的全局变量就是未被初始化的全局变量
- .data(已初始化全局变量)
- .text(代码段,二进制机器指令)
- 受保护的地址空间(NULL 就被定为这里的0地址)
当然虚拟地址空间是4G并不代表程序实际使用了4G的内存空间,虚拟地址空间会映射到实际的内存空间中。
虚拟内存的存在主要有三个作用:
- 方便进程使用不连续的大内存区
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
系统函数IO
open
- 使用
open
打开未存在的文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd;
// 打开不存在的文件
fd = open("bucunz", O_RDWR);
if(fd == -1)
{
perror("open file");
exit(1);
}
// 关闭文件
int ret = close(fd);
printf("ret = %d\n", ret);
if(ret == -1)
{
perror("close file");
exit(1);
}
}
这里 open
的第一个参数表示要打开的文件,O_RDWR
表示可读可写,其他还有 O_RDONLY
只读打开以及 O_WRONLY
只写打开,其返回文件描述符,如果出现错误则返回 -1
,这里使用 perror
在错误输出之前输出里面的内容。关闭文件使用 close
其参数为文件描述符,如果产生错误返回 -1
,成功则返回 0
。
- 创建新文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd;
// 创建新文件
fd = open("myhello", O_RDWR | O_CREAT, 0777);
if(fd == -1)
{
perror("open file");
exit(1);
}
printf("fd = %d\n", fd);
// 关闭文件
int ret = close(fd);
printf("ret = %d\n", ret);
if(ret == -1)
{
perror("close file");
exit(1);
}
}
这里通过在 open
中添加参数 O_CREAT
以在没有该文件时生成新文件。
可以看到其 fd
为 3,前面 0~2
为默认打开的,所以从 3
开始分配。
我们之前设置最后参数为 0777
表示赋予文件所有用户所有权限,但我们看到该文件的其他用户并没有写权限,这是因为Linux在赋予文件全选时需要减去一个掩码,可以通过 umask
显示,这里为0002,其取反与赋予的权限通过二进制与操纵产生最终的文件权限。
-
创建新文件,如果文件已存在则出错返回
可以在open
参数添加O_EXCL
实现,即fd = open("myhello", O_RDWR | O_CREAT | O_EXCL, 0777);
-
如果文件存在,将长度截断为0字节
可以在open
参数添加O_TRUNC
实现,即fd = open("myhello", O_RDWR | O_TRUNC);
read 与 write
read
的函数原型如下:
其返回值有三种可能:
- 返回一个大于0的数,表示读了多少字节的数据
- 返回一个 0,表示文件已经读完了
- 返回一个 -1,表示读文件出错了
write
的函数原型如下:
如果写成功,返回写入的字节数,如果出错则返回 -1。
read
与 write
函数均需要指定读取或写入文件的文件描述符,缓冲区,以及读取和写入的字节数。
下面使用 open
,read
,write
实现从一个文件读取数据放入另一个文件中:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int fd = open("test1.txt", O_RDONLY);
if(fd == -1)
{
perror("open file1");
exit(1);
}
int fd2 = open("test2.txt", O_WRONLY | O_CREAT, 0777);
if(fd2 == -1)
{
perror("open file2");
exit(1);
}
int size;
char buffer[8]; // 缓冲区
while((size=read(fd, buffer, 8)) != 0)
{
if(size == -1)
{
perror("read file1");
exit(1);
}
int ret = write(fd2, buffer, size);
if(ret == -1)
{
perror("write file2");
exit(1);
}
printf("%d\n", ret);
}
if(close(fd) == -1)
{
perror("close file1");
exit(1);
}
if(close(fd2) == -1)
{
perror("close file2");
exit(1);
}
return 0;
}
可以看到 test1.txt
一共50个字节,前六次每次读取缓冲区大小的字节数(8),最后一次读取最后2个字节。
lseek
lseek
函数原型如下:
lseek
重新定位文件偏移,offset
表示偏移量,whence
表示偏移位置,即从哪开始偏移。
whence
共有三种选项:
SEEK_SET
文件偏移被设为offset
,即从文件头开始偏移offset
字节SEEK_CUR
文件偏移被设为文件指针当前位置+offset
偏移字节数,即从当前位置开始偏移offset
字节SEEK_END
文件偏移被设为文件的大小+offset
偏移字节数,即从文件尾开始偏移offset
字节
这里需要说明的是,当从文件尾开始偏移offset
字节时,这时并不会改变文件大小,如果此时在该位置进行写入,那么中间部分会变成空洞,进行占位,除非数据被写入这些空洞,否则返回 \0
。空洞的作用有一点就是当我们下载文件时,用空洞进行占位可以需要的空间保留下来。
如果成功返回从文件头开始的偏移字节数,如果产生错误,返回 -1。
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<stdio.h>
int main(void)
{
int fd = open("./test1.txt",O_RDWR);
if( fd == -1)
{
perror("open test1.txt:");
exit(1);
}
int ret = lseek(fd,0,SEEK_END);
printf("file length = %d\n",ret);
// 文件扩展
ret = lseek(fd,2000,SEEK_END);
printf("return value = %d\n",ret);
// 实现文件拓展,需要最后一次写操作
write(fd,"a",1);
close(fd);
return 0;
}
可以看到 test1.txt
被扩展为 2051 字节了,且中间有很多空洞 ^@