⽂件在磁盘⾥ 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的 磁盘是外设(即是输出设备也是输⼊设备)
磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO
Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程)
对于 0KB 的空⽂件是占⽤磁盘空间的
⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
所有的⽂件操作本质是⽂件内容操作和⽂件属性操作
对⽂件的操作本质是进程对⽂件的操作
磁盘的管理者是操作系统
⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的
打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅ 案。不过,在认识系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接⼝中会使⽤到:
# include <stdio.h> # define ONE 0001 //0000 0001 # define TWO 0002 //0000 0010 # define THREE 0004 //0000 0100 void func ( int flags) { if (flags & ONE) printf ( "flags has ONE! " ); if (flags & TWO) printf ( "flags has TWO! " ); if (flags & THREE) printf ( "flags has THREE! " ); printf ( "\n" ); } int main () { func(ONE); func(THREE); func(ONE | TWO); func(ONE | THREE | TWO); return 0 ; }
操作⽂件,除了上⼩节的C接⼝(当然,C++也有接⼝,其他语⾔也有),我们还可以采⽤系统接⼝来进⾏⽂件访问, 先来直接以系统代码的形式,来实现个例子代码
# include <stdio.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> # include <unistd.h> # include <string.h> int main () { umask( 0 ); int fd = open( "myfile" , O_WRONLY|O_CREAT, 0644 ); if (fd < 0 ){ perror( "open" ); return 1 ; } int count = 5 ; const char *msg = "hello bit!\n" ; int len = strlen (msg); while (count--){ write(fd, msg, len); //fd: 后⾯讲, msg :缓冲区⾸地址, len: 本次读取,期望写 ⼊多少个字节的数据。 返回值:实际写了多少字节数据 } close(fd); return 0 ; }
# include <stdio.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> # include <unistd.h> # include <string.h> int main () { int fd = open( "myfile" , O_RDONLY); if (fd < 0 ){ perror( "open" ); return 1 ; } const char *msg = "hello bit!\n" ; char buf[ 1024 ]; while ( 1 ){ ssize_t s = read(fd, buf, strlen (msg)); // 类⽐ write if (s > 0 ){ printf ( "%s" , buf); } else { break ; } } close(fd); return 0 ; }
open man 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); pathname: 要打开或创建的⽬标⽂件 flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏ “ 或 ” 运算,构成 flags 。 参数 : O_RDONLY: 只读打开 O_WRONLY: 只写打开 O_RDWR : 读,写打开 这三个常量,必须指定⼀个且只能指定⼀个 O_CREAT : 若⽂件不存在,则创建它。需要使⽤ mode 选项,来指明新⽂件的访问 权限 O_APPEND: 追加写 返回值: 成功:新打开的⽂件描述符 失败: -1
mode_t理解:直接 man ⼿册,⽐什么都清楚。
open 函数具体使⽤哪个,和具体应⽤场景相关,如⽬标⽂件不存在,需要open创建,则第三个参数表⽰创建⽂件的默认权限,否则,使⽤两个参数的open。
write read close lseek ,类⽐C⽂件相关接⼝。
在认识返回值之前,先来认识⼀下两个概念: 系统调⽤ 和 库函数
上⾯的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。 ⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝

系统调⽤接⼝和库函数的关系,⼀⽬了然。
所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。
通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数
Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0, 标准输出1, 标准错
误2.
0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器
所以输⼊输出还可以采⽤如下⽅式:
# include <stdio.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> # include <string.h> int main () { char buf[ 1024 ]; ssize_t s = read( 0 , buf, sizeof (buf)); if (s > 0 ){ buf[s] = 0 ; write( 1 , buf, strlen (buf)); write( 2 , buf, strlen (buf)); } return 0 ; }

⽽现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。 对于以上原理结论我们可通过内核源码验证:
⾸先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/ sched.h (3.10.0-1160.71.1.el7.x86_64是内核版
本,可使⽤ uname -a ⾃⾏查看服务器配置, 因为这个⽂件夹只有⼀个,所以也不⽤刻意去分辨,
内核版本其实也随意)
⽂件描述符的分配规则
直接看代码
# include <stdio.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> int main () { int fd = open( "myfile" , O_RDONLY); if (fd < 0 ){ perror( "open" ); return 1 ; } printf ( "fd: %d\n" , fd); close(fd); return 0 ; }
输出发现是 fd: 3
关闭0或者2,在看
1# include <stdio.h> 2 # include <sys/types.h> 3 # include <sys/stat.h> 4 # include <fcntl.h> 5 6 int main () 7 { 8 close( 0 ); 9 //close(2); 10 int fd = open( "myfile" , O_RDONLY); 11 if (fd < 0 ){ 12 perror( "open" ); 13 return 1 ; 14 } 15 printf ( "fd: %d\n" , fd); 16 17 close(fd); 18 return 0 ; 19 }
发现是结果是: fd: 0 或者 fd 2 ,可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到
当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。
我们再看一个有关重定向代码
1 # include <stdio.h> 2 # include <sys/types.h> 3 # include <sys/stat.h> 4 # include <fcntl.h> 5 # include <stdlib.h> 6 7 int main () 8 { 9 close( 1 ); 10 int fd = open( "myfile" , O_WRONLY|O_CREAT, 00644 ); 11 if (fd < 0 ){ 12 perror( "open" ); 13 return 1 ; 14 } 15 printf ( "fd: %d\n" , fd); 16 fflush( stdout ); 17 18 close(fd); 19 exit ( 0 ); 20 }
此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,fd=1。这
种现象叫做输出重定向。常⻅的重定向有: > , >> , <
那重定向的本质是什么呢?

我们使用dup2系统调用来看一下
1# include <unistd.h> 2 3 int dup2 ( int oldfd, int newfd);
示例代码
1# include <stdio.h> 2 # include <unistd.h> 3 # include <fcntl.h> 4 5 int main () { 6 int fd = open( "./log" , O_CREAT | O_RDWR); 7 if (fd < 0 ) { 8 perror( "open" ); 9 return 1 ; 10 } 11 close( 1 ); 12 dup2(fd, 1 ); 13 for (;;) { 14 char buf[ 1024 ] = { 0 }; 15 ssize_t read_size = read( 0 , buf, sizeof (buf) - 1 ); 16 if (read_size < 0 ) { 17 perror( "read" ); 18 break ; 19 } 20 printf ( "%s" , buf); 21 fflush( stdout ); 22 } 23 return 0 ; 24 }
printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1,
但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。那追加和输⼊重定向如何完成呢?
⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东
西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。
这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤
read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函
数来进⾏。 之前博客我们提到过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/ fs.h 下,以下展⽰了该结构部分我们关系的内容:
1 struct file { 2 3 ... 4 5 struct inode * f_inode ; /* cached value */ 6 const struct file_operations *f_op; 7 8 ... 9 10 atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向 它,就会增加 f_count 的值。 11 unsigned int f_flags; // 表⽰打开⽂件的权限 12 fmode_t f_mode; // 设置对⽂件的访问模式 , 例如:只读,只写等。所有 的标志在头⽂件 <fcntl.h> 中定义 13 loff_t f_pos; // 表⽰当前读写⽂件的位置 14 15 ... 16 17 } __attribute__((aligned( 4 ))); /* lest something weird decides that 2 is OK */ 18
值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构
体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。
1 struct file_operations { 2 struct module * owner ; 3 // 指向拥有该模块的指针; 4 loff_t (*llseek) ( struct file *, loff_t , int ); 5 //llseek ⽅法⽤作改变⽂件中的当前读 / 写位置 , 并且新位置作为 ( 正的 ) 返回值 . 6 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 7 // ⽤来从设备中获取数据 . 在这个位置的⼀个空指针导致 read 系统调⽤以 - EINVAL("Invalid argument") 失败 . ⼀个⾮负返回值代表了成功读取的字节数 ( 返回值是⼀个 "signed size" 类型 , 常常是⽬标平台本地的整数类型 ). 8 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 9 // 发送数据给设备 . 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序 . 如果⾮负 , 返 回值代表成功写的字节数 . 10 ssize_t (*aio_read) ( struct kiocb *, const struct iovec *, unsigned long , loff_t ); 11 // 初始化⼀个异步读 -- 可能在函数返回前不结束的读操作 . 12 ssize_t (*aio_write) ( struct kiocb *, const struct iovec *, unsigned long , loff_t ); 13 // 初始化设备上的⼀个异步写 . 14 int (*readdir) ( struct file *, void *, filldir_t ); 15 // 对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录 , 并且仅对 ** ⽂件系统 ** 有⽤ . 16 unsigned int (*poll) ( struct file *, struct poll_table_struct *); 17 int (*ioctl) ( struct inode *, struct file *, unsigned int , unsigned long ); 18 long (*unlocked_ioctl) ( struct file *, unsigned int , unsigned long ); 19 long (*compat_ioctl) ( struct file *, unsigned int , unsigned long ); 20 int (*mmap) ( struct file *, struct vm_area_struct *); 21 //mmap ⽤来请求将设备内存映射到进程的地址空间 . 如果这个⽅法是 NULL, mmap 系统调⽤返 回 -ENODEV. 22 int (*open) ( struct inode *, struct file *); 23 // 打开⼀个⽂件 24 int (*flush) ( struct file *, fl_owner_t id); 25 //flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤ ; 26 int (*release) ( struct inode *, struct file *); 27 // 在⽂件结构被释放时引⽤这个操作 . 如同 open, release 可以为 NULL. 28 int (*fsync) ( struct file *, struct dentry *, int datasync); 29 // ⽤⼾调⽤来刷新任何挂着的数据 . 30 int (*aio_fsync) ( struct kiocb *, int datasync); 31 int (*fasync) ( int , struct file *, int ); 32 int (*lock) ( struct file *, int , struct file_lock *); 33 //lock ⽅法⽤来实现⽂件加锁 ; 加锁对常规⽂件是必不可少的特性 , 但是设备驱动⼏乎从不实现 它 . 34 ssize_t (*sendpage) ( struct file *, struct page *, int , size_t , loff_t *, int ); 35 unsigned long (*get_unmapped_area)( struct file *, unsigned long , unsigned long , unsigned long , unsigned long ); 36 int (*check_flags)( int ); 37 int (*flock) ( struct file *, int , struct file_lock *); 38 ssize_t (*splice_write)( struct pipe_inode_info *, struct file *, loff_t *, size_t , unsigned int ); 39 ssize_t (*splice_read)( struct file *, loff_t *, struct pipe_inode_info *, size_t , unsigned int ); 40 int (*setlease)( struct file *, long , struct file_lock **); 41 }; 42
file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都
对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽
完成了Linux设备驱动程序的⼯作。
介绍完相关代码,⼀张图总结:

上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过
struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系
统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。
缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。
读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤⼤快于对磁盘的操作,故应⽤缓冲区可⼤⼤提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。
标准I/O提供了3种类型的缓冲区。
全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。 ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤ 操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准 I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏ I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。 ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
1. 缓冲区满时; 2. 执⾏flush语句;