我们都知道,linux下一切皆文件的概念,那么具体的运行机制是如何的呢,为什么通过操作文件即可操作对应的硬件,而具体的过程如下:
Linux 通过 VFS(虚拟文件系统)将硬件设备抽象为文件接口。当用户通过文件描述符(fd)发起系统调用时,内核通过 fd 找到对应的struct file和inode,从 inode 中提取设备号,并在内核驱动表中找到已注册的struct file_operations。随后,内核调用该结构体中对应的函数(如.read/.write),由该函数通过驱动配置操作硬件控制器进行数据收发。硬件本身的初始化由驱动加载或open时完成。
因为有这么一整套完整的运行机制,所以内核就只需向外暴露一个文件的接口供给用户使用就可以,那么我们接下来就具体说一说对应的文件的操作。
一.文件IO
1.介绍
IO自如其名 指input输入和output输出
文件IO:是操作系统内核直接提供的、基于“文件描述符(fd)”的、无用户态缓冲区的底层输入输出接口,在Linux下通常指POSIX(可移植操作系统接口)标准的系统调用
他是直接基于文件描述符(fd)的接口,是用户能触及内核的最短路径(无用户态缓存),所以实时性会很强,我们在需要高实时性和更底层的操作时会用到很多,比如网络编程。
他有几个特点
- 不提供
缓冲机制,每次读写操作都引起系统调用 - 核心概念是
文件描述符(fd) - 访问
各种类型文件 - Linux下,
标准IO基于文件IO实现
2.文件描述符(fd)
文件描述符:文件描述符是一个非负整数(如 3, 4, 5...),它是操作系统内核为了高效管理已被打开的文件,而分配给进程的一个索引号。
每一个打开的文件都对应一个文件描述符,文件IO就是通过这个描述符完成的
文件描述符是从0开分配的,依次递增
0, 1, 2表示标准输入,标准输出,错误
3.文件打卡与关闭
下面我们来深入接触一下系统提供的文件IO的函数接口
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
第一个参数pathname是文件的路径名称
第二个参数flags是指对文件的操作类型,比如只读,只写等
第三个参数是对文件的权限管理(只有在新建文件时有效)
这里插入一下,在linux中通过 ll 命令我们可以查看文件的详细信息,如下
-rw-rw-r--1 kickpi kickpi 0 Jul 31 19:37 test.txt
我们可以看到对于test.txt文件的第一列-rw-rw-r--有此文件的类型,权限信息等,
-rw-:这几个从左到右分别是指文件类型,和用户组(user)权限,r:读取,w:写入 ,最后一个是执行权限(-就是没有执行权限,x就是有执行权限)。
rw-:这几个是所属组(Group)权限,可读可写不可执行
r--:这几个是其他用户权限
我们经常会看到给文件赋予权限的命令chmod 666 test.txt就是改变这几个参数,666对应用户组,所属组合其他用户组。至于数字,比如说用户组rw-就是按二进制来算,-是0,w是010对应2,r是100对应4 ,所以这个数就是0+2+4=6.其他同理。
好了,接着我们回归整体,介绍文件IO的操作
详细说一说第二个参数的flags使用,使用多个参数可以用 | 符号结合
O_RDONLY | 以只读方式打开文件。 |
O_WRONLY | 以只写方式打开文件。 |
O_RDWR | 以读写方式打开文件。 |
| 如果文件不存在则创建它。 使用此标志时,必须提供第三个参数 mode来指定新文件的权限。 |
O_TRUNC | 如果文件已存在且以写方式打开,则将其长度截断为0(清空内容)。 |
O_APPEND | 以追加方式打开。每次执行write写入时,数据都自动添加到文件末尾。 |
O_EXCL | 与O_CREAT同时使用时,如果文件已存在则打开失败。这可用于测试文件是否存在,且该操作是原子的。 |
O_NONBLOCK | 以非阻塞方式打开文件。用于设备文件、FIFO或Socket等。 如果资源不可用, open调用会立即返回,而不会让程序一直等待。 |
O_SYNC | 使每次write操作同步进行,即等待物理I/O操作完成后才返回。这会确保数据被写入磁盘,但会降低性能。 |
O_NOCTTY | 如果pathname指向一个终端设备,则不会将该设备分配为进程的控制终端。 |
O_DIRECTORY | 如果pathname不是目录,则open调用失败。 |
O_NOFOLLOW | 如果 |
关闭文件的函数
#include <unistd.h>
int close(int fd);
参数fd是文件描述符。成功时返回0,错误时返回-1。
下面给出示例代码:
// 示例代码:使用文件IO打开、创建、关闭文件 #include <stdio.h> // 标准输入输出头文件 #include <unistd.h> // POSIX操作系统API头文件,包含close函数 #include <errno.h> // 错误号定义头文件 #include <fcntl.h> // 文件控制选项头文件,包含open函数和标志 int main() { // 打开(或创建)文件"./test.txt",以读写模式,如果文件不存在则创建,权限为0666 int fd = open("./test.txt", O_RDWR | O_CREAT, 0666); // 检查文件描述符是否有效(open失败返回-1) if (fd == -1) { // 打印错误信息 printf("%s", strerror(errno)); return -1; // 返回错误代码 } // 打印成功打开的文件描述符值 printf("fd = %d\n", fd); // 关闭文件描述符,释放资源 close(fd); return 0; // 程序正常退出 }输出结果如下
fd=3
4.文件的写入,读取等
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t count);
这是文件的写入,其中buf是写入数据的缓存区,count是写入数据的大小
ssize_t read(int fd, void *buf, size_t count);
这是文件的读取,其中buf是接受数据的缓存区,count是读取数据大小,count要小于buf。其次是读取的时候要留一位给换行符。
读取和写入都设计要一个概念就是文件偏移量后移,就是指无论你写入数据还是读取数据,都会把指向文件内容的位置移动到最后一个处理的字符那里,所以要使用文件定位函数
#include <unistd.h>
off_t lseek(int fd, off_t offset, intt whence);
第三个参数whence是锁定开始移动的位置,第二个参数offset是移动多少字节,这个函数可以帮助我们调整目前文件内容所在的位置。下面是第三个参数的具体值和含义
SEEK_SET | 0 | 文件开头 | 绝对定位。offset必须 >= 0。把书签直接插到指定的页数。 |
SEEK_CUR | 1 | 当前位置 | 相对定位。offset可正可负。书签从当前页开始,往前或往后翻指定的页数。 |
SEEK_END | 2 | 文件末尾 | 相对定位。offset通常为负数(往后退),也可以为正数(制造空洞)。书签从最后一页的末尾开始往前退。 |
看代码
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
int main(){
int fd = open("./test.txt",O_RDWR|O_CREAT,0666);
char buf[12] = {0};
if(fd==-1)
{
printf("%s",strerror(errno));
return -1;
}
if(write(fd,"hello world",sizeof("hello world"))<0){
printf("%s",strerror(errno));
return -1;
}
lseek(fd, 0, SEEK_SET);
int ret = read(fd,buf,sizeof(buf));
if(ret<=0){
printf("%s",strerror(errno));
return -1;
}
printf("fd=%d\n",fd);
printf("buf=%s\n",buf);
close(fd);
return 0;
}
输出
$ make run
fd=3
buf=hello world
这就是完整的打开,写入,调整位置,读取,关闭文件的流程
了解这些就差不多了,还有一些其他的函数有特定的功能,我放在文章最后的链接里,可以自行查看
二.标准IO
标准IO和文件IO是有区别的,他是基于文件IO用ANSI C标准进行定义的,加入了用户缓存区这一环节,正是有这个缓存机制,减少了系统的调用环节,能实现更高的效率,c语言中的stdio.h就是其标准库。
标准IO的操作是用一个FILE结构体来存放文件信息的,他会将信息先存入用户态缓存区,到达一定的数据量后再推送出去,这样的操作就像流操作,所以标准IO又被称为 流 。如下FILE结构体的定义:
#ifndef _FILE_DEFINED
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
下面我们说一说关于多出来的这个缓存区
缓冲区不是虚无缥缈的概念,它就是 C 库(glibc)为每个FILE *流在用户态内存(堆上)分配的一块连续的字符数组(比如 8192 字节)。
当使用标准IO的写入函数fwrite时,会先将写入的数据存入缓存区,达到一定条件时,才会调用系统IO函数来进行系统调度,这时候有几种机制,有几个指针
_IO_write_base:指向缓冲区的起始地址。_IO_write_ptr:指向当前可写入的位置。数据从这里开始存放。_IO_write_end:指向缓冲区的末尾地址
模式 场景 解释
| 全缓冲(_IOFBF) | _IO_write_ptr == _IO_write_end(写满)或者程序正常 exit/fclose | 满了才调用write。默认用于普通磁盘文件。 |
| 行缓冲(_IOLBF) | 写入的数据里包含 | 遇到回车就调用write。默认用于终端(stdout)。注意:如果缓冲区满了但没换行,也会刷。 |
| 无缓冲(_IONBF) | _IO_write_ptr根本不用(缓冲区长度为 0) | 每次fwrite直接调用write,数据不经过用户态缓存。默认用于stderr。 |
stdin/stdout默认是行缓冲stderr没有缓冲
1.文件的打开关闭
FILE *fopen (const char *path, const char *mode);
path是文件路径和文件名
mode是和文件IO一样指定打开文件的形式
| 模式字符串 | 文件不存在时 | 文件存在时 | 读写位置(偏移量) | 核心用途 |
|---|---|---|---|---|
"r"(只读) | 报错返回 NULL | 正常打开 | 文件开头 | 必须文件已存在,只读不写(如读取配置文件)。 |
"w"(只写) | 创建新文件 | 清空为 0 字节(内容全丢!) | 文件开头 | 从头开始写,覆盖旧内容(危险操作,类似O_TRUNC)。 |
"a"(追加) | 创建新文件 | 保留原有内容 | 文件末尾 | 所有写入的数据自动追加到文件末尾(类似O_APPEND,不能回头改旧数据)。 |
在基础后面加上+ 表示可读可写
"r+" | 报错返回 NULL | 正常打开(不清空) | 文件开头 | 用于“修改现有文件”的场景(读旧数据,写新数据覆盖)。 注意:读写切换时必须 fseek或fflush,否则容易混乱。 |
"w+" | 创建新文件 | 清空为 0 字节 | 文件开头 | 用于“从头创建并读写”。相当于先清空,再读写。 |
"a+" | 创建新文件 | 保留原有内容 | 文件末尾(读时从开头读,写时强制末尾) | 读可以从开头看,但写永远跳到末尾追加 |
还有一个"b"参数 ,他是为了阻止 C 库在\n和\r\n之间做多余的事,可以保证读取写入的正确性
在windows系统中\r 代表回到行首 \n代表向下平移到下一行,\r\n结合就是到下一行行首,如果没用参数b,windows系统下可能对\n做处理把他转换成\r\n,会引起歧义。linux只有\n不会引起歧义。做移植的时候就必须加这个参数了
int fclose(FILE *stream)
成功返回0,失败返回EOF(-1)
2.读写文件操作
读取:
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream); //宏
int getchar(void);
fgetc是读取流的一个字符,
getc也是读取流的一个字符,但是他是宏
getchar是获取输入的字符与fgetc(stdin)一样
char *fgets(char *s, int size, FILE *stream);
这个是读取整行数据的
写入:
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
成功时返回写入的字符;出错时返回EOF(-1)
- 这几个函数都是写入单个字符的
int puts(const char *s); int fputs(const char *s, FILE *stream);
puts将缓冲区s中的字符串输出到stdout,并追加’\n’fputs将缓冲区s中的字符串输出到stream,不追加 ‘\n’
再介绍这两个函数,高效读写:
size_t fread(void *ptr, size_t size, size_t n, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *fp);
| 参数 | 含义 | 通俗理解 |
|---|---|---|
ptr | 内存缓冲区指针 | 读取时:数据放这里(fread);写入时:数据从这里取(fwrite)。 |
size | 每个元素的大小(字节数) | 比如你要存int,这里就填sizeof(int);要存char,就填1。 |
nmemb | 要读/写的元素个数 | 你想一次性处理多少个元素。 |
stream | 文件流指针 | fopen返回的FILE*。 |
fread和fwrite是标准I/O中“批量数据搬运工”。相比于fgetc(一次一个字符)和fputs(一次一行),它们直接操作内存块,非常适合读写二进制数据(如结构体、数组、图片数据)或大块文本。
接着补充对于文件内容位置操作的函数
| 函数 | 原型 | 移动基准 | 返回值 | 是否清除feof/ferror | 是否支持超大文件 (>2GB) |
|---|---|---|---|---|---|
rewind | void rewind(FILE *fp); | 固定到开头 | 无 (void) | 是(隐式清除) | 是(内部使用fsetpos或 64位偏移) |
fseek | int fseek(FILE *fp, long offset, int whence); | SEEK_SET/CUR/END+ 任意偏移量 | 成功0,失败-1 | 否(标志保留) | 否(long在32位系统限制2GB) |
ftell | long ftell(FILE *fp); | 获取当前位置 | 成功返回偏移量,失败返回-1L | 不影响标志 | 否(同样受long大小限制) |
fgetpos | int fgetpos(FILE *fp, fpos_t *pos); | 获取当前位置(存入pos) | 成功0,失败非0 | 不影响标志 | 是(fpos_t设计支持大文件) |
fsetpos | int fsetpos(FILE *fp, const fpos_t *pos); | 设置到pos指定的位置 | 成功0,失败非0 | 否(标志保留) | 是(fpos_t设计支持大文件) |
到这里就差不多了,这里在附上基本操作的代码
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
int main(){
FILE *fp=fopen("test.txt","r+");
if(fp==NULL){
perror("fopen");
return -1;
}
fputs("wsyunnand",fp);
rewind(fp);
int buf;
buf=fgetc(fp);
if(buf!=EOF){
printf("read:%c\n",buf);
}
char read[100] = {0};
int i=fread(read,1,100,fp);
printf("read all:%d",i);
printf("read all:%s",read);
fclose(fp);
return 0;
}
文件内容是这个
wsyunnand
yeyeyessssssssssss
ddddddddd
输出
$ make run
read:w
read all:39read all:syunnand
yeyeyessssssssssss
ddddddddd
最后把一些其他标准IO和文件IO的一些其他函数放在这个链接:
linux标准IO和文件IO函数-CSDN博客