【Linux】I/O操作
目录
1. 整体学习思维导图
2. 理解文件
2.1 文件是什么?
2.2 回顾C语言库函数的文件操作
2.3 stdin/stdout/stderr
2.4 系统的文件I/O操作
2.4.1 了解位图标记位方法(宏)
2.4.2 认识系统I/O常用调用接口
2.5 对比C文件操作函数和系统调用函数
2.5.1 fd是什么?
2.5.2 重定向原理
2.5.2.1 回顾重定向( < , >, >>)
2.5.2.2 探究原理
2.5.2.3 重谈重定向
3. 理解Linux下一切皆文件
缓冲区机制
4.1 什么是缓冲区
4.2 为什么要引入缓冲区机制
4.3 缓冲机制
4.4 C语言的缓冲区
5. 尝试自定义封装lib.c库 - file
1. 整体学习思维导图
2. 理解文件
2.1 文件是什么?
-
问题引入:一个文件大小为0,那么这个文件在磁盘上需不需要占用空间?
答案肯定是需要占用空间的,因为我们需要存储这个文件的文件名,文件类型,文件时间等等信息,这些被称作为文件的属性!
由此我们可以得知一个 文件 = 内容 + 属性。
-
我们学习Linux之后,知道Linux下一切皆是文件,存在键盘文件,显示器文件,鼠标文件....,但是我们知道这些输入输出设备都是硬件设备,甚至于我们磁盘上的存储文件也是硬件的一部分,那么我们是怎么操作这些硬件文件的呢--->OS(操作系统),一旦谈到操作系统的管理必然涉及 描述 + 组织,对一个文件的描述包含对文件的内容和属性!
-
那么操作一个文件又怎么理解,我们之前学习过c语言的对文件的操作函数,而这意味着我们需要一个进程去执行对应的文件操作代码,所以对文件的操作本质就是进程对文件的操作!
2.2 回顾C语言库函数的文件操作
FILE *fopen(const char *path, const char *mode);
// return a FILE pointer. Otherwise, NULL is returned and errno is set to indicate the error.
// 成功返回一个FILE类型的指针,失败返回NULL
我们执行这段代码后,会在我们对应的目录下创建一个log.txt的文件,那么为什么会在这个路径创建?
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_23]$ ll
total 20
-rw-rw-r-- 1 ouyang ouyang 65 Feb 23 14:09 makefile
-rwxrwxr-x 1 ouyang ouyang 8544 Feb 23 14:11 Test
-rw-rw-r-- 1 ouyang ouyang 200 Feb 23 14:11 Test.c
我们前面说过是进程对文件进行操作,我们去查看对应的进程信息会发现有着路径信息,而这个文件的路径就是拼凑创建的!
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps ajx | grep Test | grep -v grep
26512 28688 28688 26512 pts/0 28688 R+ 1001 0:36 ./Test
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ls /proc/28688 -l
total 0
dr-xr-xr-x 2 ouyang ouyang 0 Feb 23 14:16 attr
-rw-r--r-- 1 ouyang ouyang 0 Feb 23 14:16 autogroup
-r-------- 1 ouyang ouyang 0 Feb 23 14:16 auxv
-r--r--r-- 1 ouyang ouyang 0 Feb 23 14:15 cgroup
--w------- 1 ouyang ouyang 0 Feb 23 14:16 clear_refs
-r--r--r-- 1 ouyang ouyang 0 Feb 23 14:16 cmdline
-rw-r--r-- 1 ouyang ouyang 0 Feb 23 14:16 comm
-rw-r--r-- 1 ouyang ouyang 0 Feb 23 14:16 coredump_filter
-r--r--r-- 1 ouyang ouyang 0 Feb 23 14:16 cpuset
lrwxrwxrwx 1 ouyang ouyang 0 Feb 23 14:16 cwd -> /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_2_23
-r-------- 1 ouyang ouyang 0 Feb 23 14:16 environ
lrwxrwxrwx 1 ouyang ouyang 0 Feb 23 14:16 exe -> /home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_2_23/Test
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
-
文件写入信息
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_23]$ cat log.txt
Hello fwriteHello fwriteHello fwriteHello fwriteHello fwrite
-
文件读取信息
2.3 stdin/stdout/stderr
#include <stdio.h>
extern FILE *stdin; // 标准输入 键盘文件
extern FILE *stdout; // 标准输出 显示器文件
extern FILE *stderr; // 标准输出 显示器文件
-
打开文件方式
Truncate清空文件的内容
-
fseek / ftell / rewind函数回顾
2.4 系统的文件I/O操作
2.4.1 了解位图标记位方法(宏)
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_23]$ ./Bite
One!
One!
Two!
One!
Two!
Three!
One!
Two!
Three!
Four!
2.4.2 认识系统I/O常用调用接口
-
open
SYNOPSIS
#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);
-
pathname:文件名称
-
flags:宏标记位
-
O_RDONLY:只读打开
-
O_WRONLY:只写打开
-
O_CREAT:若文件不存在创建
-
O_APPEND:追加写
-
O_TRUNC:清空文件内部内容
-
-
mode:权限设置 权限
如果成功会返回一个文件描述符(fd),失败返回-1
-
C的调用和系统调用的对比
c的库函数底层就是封装系统调用来实现的!
w Truncate file to zero length or create text file for writing. The stream is positioned at the beginning of the file.
int main()
{
// c语言库函数封装的w 和 系统调用的对比,结果是一样的
// 注意我们打开不存在的文件时需要设置权限,最终权限 = 起始权限 & (~umask)
// 如果想要消除umask的影响,直接调用umask()函数接口
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
close(fd);
return 0;
}
a Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
-
文本写入和二进制写入
可能对于我们使用c语言封装的库函数时,我们写入文本和写入二进制需要调用不同的接口:
-
fwrite / fread : 这是写入二进制文件需要调用的接口
-
fputs/... : 这是写入文本文件需要调用的接口
但是对于系统调用来说不管你是文本还是二进制写入都是调用open函数即可!
-
系统写入文件
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ cat log.txt
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
-
系统读取文件
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ make
gcc -o Test Test.c
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ ./Test
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
2.5 对比C文件操作函数和系统调用函数
我们会发现对于C的库函数的返回值是一个FILE*,FILE是一个结构体;系统调用接口返回的是fd(文件描述符),是一个整型,我们可以知道的是FILE内部一定含有fd!
2.5.1 fd是什么?
来看以下代码,我们创建打开三个文件并且打印他们的fd返回值我们会发现一些有趣的问题:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ ./Test
fd1:3
fd2:4
fd3:5
fd返回的是数组下标,那么为什么返回的下标从三开始呢?我们前面提到过系统会默认给我们打开三个文件:stdin/stdout/stderr,而他们的下标正好对应的是0,1,2!
前面我们提到过每个文件都是由内容+属性组成,而文件存储在磁盘,我们应用层(用户)想要使用文件需要经过OS(系统)进行操作,而OS对文件进行管理需要进行先描述+在组织,所以我们需要一个结构体(struct file)对文件进行描述,同样我们知道冯诺依曼体系结构CPU不可以直接和硬件交互,需要中间的缓冲区(内存),磁盘和OS也是如此,中间存在一个缓冲区,将磁盘文件预加载(拷贝)到缓冲区,OS对于文件的管理就是对文件结构体链表的增删查改!
这张图就是整个用户->OS->磁盘对于文件的操作,并且还有一个重要点,文件可以被多个进程打开,这意味着一个文件要想关闭回收资源必须要所有的进程都不再使用这个文件才可以,所以文件结构体(struct file)内部有一个引用计数表示有多少个进程打开这个文件,只有等到计数归零才回收资源,这一点的设计和智能指针大体相同!
2.5.2 重定向原理
2.5.2.1 回顾重定向( < , >, >>)
-
> 输出重定向(覆盖)
echo "Hello" > log.txt
-
>> 输出重定向(追加)
echo "Hello" >> log.txt
-
< 输入重定向
wc -l < log.txt // 统计文件行数
2.5.2.2 探究原理
我们平时可能执行以下指令:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ echo "Hello"
Hello
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ echo "Hello" > log.txt
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ cat log.txt
Hello
echo指令本意是向显示器文件输入,但是为什么加上重定向>和重定向的文件名,即可实现原本向显示器输入的文本转到log.txt的呢?
想要分析探究重定向的原理离不开一张表,那就是文件描述符表!来看下图:
当我们将原本输入到stdin的内容,经过重定向之后,myfile被新的fd指针指向(fd=0),这样使得原本应该输入到stdin文件内容的东西,输出到了myfile内部之中了!而这种操作必然涉及到更改file* fd_array[]的指针指向的操作,而系统给我们提供了一个调用接口函数dup2()
NAME
dup, dup2, dup3 - duplicate a file descriptor
SYNOPSIS
#include <unistd.h>
int dup2(int oldfd, int newfd);
这个接口简单说就是将newfd指向的文件结构体拷贝给oldfd进行指向,那么我们要实现一个echo "Hello" > log.txt
重定向,调用的函数是这样写的 dup2(1, fd)
。
额外细节注意点:
-
文件描述符的分配原则:最小的,没有使用的fd作为最新的fd分配给用户
-
重定向:文件打开的方式 + dup2
以下代码的作用是,打开一个文件T_log.txt,关闭标准输出流(fd = 1),将原本输出流的fd分配给文件的fd,这样我们在使用stdin输入时打印printf,内容就会打印到T_log.txt文件了!
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_24]$ cat T_log.txt
hello Woeld
hello
Woeld
ouyang
oeld
2.5.2.3 重谈重定向
我们知道系统默认会打开三个流,stdin/stdout/stderr,我们来看以下代码:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26]$ ./code > log.txt
Hello stderr
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26]$ cat log.txt
Hello printf
Hello cout
Hello stdout
我们知道stdout和stderr都是向显示器文件进行打印的,那么为什么代码执行重定向操作只有stdout的数据拷贝到log.txt文件呢?其实我们的重定向操作是简写的:我们所输入的指令./code > log.txt
=>./code 1 > log.txt
,重定向将我们的write内容拷贝到指定的文件缓冲区之中。
-
我们怎么将stdout 和 stderr 的内容打印到一个指定文件:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26]$ ./code 1 > log.txt 2>&1
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26]$ cat log.txt
Hello printf
Hello cout
Hello stdout
Hello stderr
3. 理解Linux下一切皆文件
通过前面的了解,我们知道对一个文件的操作无非最重要的是两个接口:read
和write
,读取文件内容,向文件写入内容。
我们也知道在Linux中将硬件设备也全都"转化"成了一个个文件,为什么要怎么做呢?不同的设备具有不同的特性和操作方式编码型号,如果我们去操作每一个硬件时我们需要设计不同的调用API,这无疑加大了学习的成本,为了更加方便使用Linux封装了这些硬件将他们转换成了一个个文件,文件的接口调用统一,使得操作方便!
-
对于硬件的管理OS需要先描述在组织
-
封装文件管理
-
源码部分:
struct file {
...
struct inode* f_inode; /* cached value */
const struct file_operations* f_op;
... atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向 它,就会增加f_count的值。
unsigned int f_flags; // 表⽰打开⽂件的权限
fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos; // 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4)));
struct file_operations {
struct module* owner;
// 指向拥有该模块的指针;
loff_t (*llseek)(struct file*, loff_t, int);
// llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read)(struct file*, char __user*, size_t, loff_t*);
// 发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序.
// 如果⾮负, 返回值代表成功写的字节数.
ssize_t (*aio_read)(struct kiocb*, const struct iovec*, unsigned long, loff_t);
// 初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write)(struct kiocb*, const struct iovec*, unsigned long, loff_t);
// 初始化设备上的⼀个异步写.
int (*readdir)(struct file*, void*, filldir_t);
// 对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录,
// 并且仅对**⽂件系统**有⽤.
unsigned int (*poll)(struct file*, struct poll_table_struct*);
int (*ioctl)(struct inode*, struct file*, unsigned int, unsigned long);
long (*unlocked_ioctl)(struct file*, unsigned int, unsigned long);
long (*compat_ioctl)(struct file*, unsigned int, unsigned long);
int (*mmap)(struct file*, struct vm_area_struct*);
};
上图中的外设,每个设备都可以有自己的read、write,但⼀定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取Linux系统中绝大部分的资源!!这便是“linux下⼀切皆文件”的核心理解。
4. 缓冲区机制
4.1 什么是缓冲区
-
缓冲区是内存的一段空间
-
缓冲区有两种:一种是语言级的缓冲区,一种是系统级的缓冲区
我们调用的C函数printf/scanf/fputs/fwrite会先存储在语言层的缓冲区,根据缓冲类型进行刷新到文件内核缓冲区。
-
缓冲类型
-
无缓冲 ---> 立即刷新
-
满缓冲 ---> 等待缓冲区满之后刷新
-
行缓冲 ---> 行刷新,遇到\n就刷新,没有\n直到行缓冲区满刷新,大小大致是1024字节
-
我们使用的系统调用+fd,如:write会直接将信息存储在文件内核缓冲区,而文件内核缓冲区什么时候刷新,怎么方式刷新到磁盘由OS决定,但是我们一般说只要将数据交给OS=数据交给了硬件!
-
上述所有的数据流动都是从一个缓冲区拷贝到另一个缓冲区,我们可以说数据流动的本质就是拷贝!
4.2 为什么要引入缓冲区机制
如果没有缓冲区意味着我们每输入一段数据就需要调用系统接口进行写入到硬件,简单说你买了快递每到一个快递都会打电话叫你立即去取,而有了缓冲区就相当于菜鸟驿站,将数据(快递)存放在缓冲区(驿站),由OS(我们)决定怎么去,存放多少再去取!这无疑大大提高了使用者的效率!
4.3 缓冲机制
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ ./code
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ ll
total 20
-rwxrwxr-x 1 ouyang ouyang 8592 Feb 26 10:23 code
-rw-rw-r-- 1 ouyang ouyang 353 Feb 26 10:23 code.c
-rw-rw-r-- 1 ouyang ouyang 0 Feb 26 10:23 log.txt
-rw-rw-r-- 1 ouyang ouyang 58 Feb 26 09:45 makefile
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ cat log.txt
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$
我们会发现log.txt
并没有如预期一样被写入我们的Hello World,导致这样的原因是因为printf的内容被暂时存放到语言层的缓冲区,而stdout的缓冲机制刷新没有触发,语言层的缓冲区并没有刷新到系统文件内核的缓冲区。
解决方式:
-
Fflush
-
调用系统接口 write
-
stderr的缓冲机制是立即刷新
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ make
gcc -o code code.c
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ ./code
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ cat log.txt
hello world: Success
4.4 C语言的缓冲区
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ gcc -o File File.c
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ ll
total 24
-rw-rw-r-- 1 ouyang ouyang 338 Feb 26 10:34 code.c
-rwxrwxr-x 1 ouyang ouyang 8672 Feb 26 10:48 File
-rw-rw-r-- 1 ouyang ouyang 331 Feb 26 10:48 File.c
-rw-rw-r-- 1 ouyang ouyang 58 Feb 26 09:45 makefile
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ ./File > log.txt
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_26_2]$ cat log.txt
Hello write
Hello Printf
Hello fwrite
Hello Printf
Hello fwrite
-
我们发现重定向到
log.txt
后,系统调用接口执行一次,而C封装的却打印了双份!这是为什么呢?我们可见代码中我们在程序结束前fork()
创建了一个子进程,子进程和父进程指向同一块数据代码,子进程发生写时拷贝也存在自己的缓冲区,前面我们C的缓冲区内容并没有刷新(缓冲模式的变化),所以父子进程各有一个,一起执行向文件中写入,所以有了两份数据,这也侧面说明write
的系统调用没有缓冲区。
-
重定向到文件后,缓冲模式变为全缓冲,导致
printf
和fwrite
的缓冲区未及时刷新。 -
write
是系统调用,无缓冲区,不受fork()
影响,因此只输出一次。
最终现象的本质是:标准库缓冲区的复制 + 缓冲模式变化,而非单纯的“子进程写时拷贝”。
-
输出
stdout
时,缓冲模式是行缓冲 -
重定向到
log.txt
时,缓冲模式变为了全缓冲,即使存在\n
也不会进行刷新!
-
对比我们平时使用的
FILE* pf
,其中FILE
结构体必然封装了fd
,通过系统调用接口实现文件操作
5. 尝试自定义封装lib.c库 - file
#pragma once
// 缓冲模式
#define NONE_FLUSH 1<<0 // 无缓冲
#define LINK_FLUSH 1<<1 // 行缓冲
#define FULL_FLUSH 1<<2 // 满缓冲
#define BUFFER_SIZE 1024
typedef struct MyFile{
int _fd; // 文件描述符
int _flush_stauts; // 缓冲模式
char _buffer[BUFFER_SIZE]; // 缓冲区大小
const char* _filename; // 对应的文件名
int _mod; // 打开文件方式
int _truesize;
}MyFile;
MyFile* Myfopen(const char* address, const char* mod);
int Myfwrite(const char* msg, int size, int num, MyFile* fp);
void Myflush(MyFile* fp);
void Myfclose(MyFile* fp);
#include "MyFile.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
MyFile* Buy_MyFile()
{
MyFile* fp = (MyFile*)malloc(sizeof(MyFile));
fp->_fd = -1;
fp->_flush_stauts = NONE_FLUSH;
fp->_filename = NULL;
fp->_mod = 0;
fp->_truesize = 0;
return fp;
}
int GetMod(const char* mod)
{
int final_mod = 0;
if(strcmp(mod, "w"))
{
// create + again
final_mod = O_CREAT | O_TRUNC | O_WRONLY;
}
else if(strcmp(mod, "a"))
{
// create + append
final_mod = O_CREAT | O_APPEND | O_WRONLY;
}
else if(strcmp(mod, "r")){
final_mod = O_RDONLY;
}
else{
// ...
}
return final_mod;
}
MyFile* Myfopen(const char* address, const char* mod)
{
// 创建一个文件对象
MyFile* fp = Buy_MyFile();
fp->_filename = address;
fp->_mod = GetMod(mod);
fp->_fd = open(fp->_filename, fp->_mod, 0666);
if(fp->_fd == -1)
{
perror("open fail!\n");
exit(1);
}
fp->_flush_stauts = LINK_FLUSH;
return fp;
}
int Myfwrite(const char* msg, int size, int num, MyFile* fp)
{
while(num--)
strncpy(fp->_buffer, msg, size);
fp->_truesize += size;
// 判断缓冲方式
if(fp->_flush_stauts == NONE_FLUSH)
{
write(fp->_fd, fp->_buffer, fp->_truesize);
fp->_truesize = 0;
return 1;
}
else if(fp->_flush_stauts == LINK_FLUSH && fp->_buffer[size] == '\n')
{
write(fp->_fd, fp->_buffer, fp->_truesize);
fp->_truesize = 0;
return 1;
}
else if(fp->_flush_stauts == FULL_FLUSH && fp->_truesize == BUFFER_SIZE)
{
write(fp->_fd, fp->_buffer, fp->_truesize);
fp->_truesize = 0;
return 1;
}
else{
// ...
return -1;
}
return -1;
}
void Myflush(MyFile* fp)
{
write(fp->_fd, fp->_buffer, fp->_truesize);
fp->_truesize = 0;
}
void Myfclose(MyFile* fp)
{
close(fp->_fd);
// 回收资源
free(fp);
}
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "MyFile.h"
int main()
{
// File
MyFile* fp = Myfopen("log.txt", "w");
if(NULL == fp)
{
perror("fopen fail!\n");
exit(1);
}
// 写入数据
const char* msg = "Hello MyFile!\n";
int i = 5;
while(i--)
{
Myfwrite(msg, strlen(msg), 1, fp);
Myflush(fp);
}
Myfclose(fp);
return 0;
}