1、 一切皆文件
1.1、文件的概念
在 Linux 中,有一句经典的话叫做:一切皆文件。这句话是站在内核的角度说的,因为 在内
核中所有的设备 (除了网络接口) 都一律使用 Linux 独有的虚拟文件系统 (VFS) 来管 理。这样做的
最终目的,是将各种不同的设备用“文件”这个概念加以封装和屏蔽,简化应 用层编程的难度。文
件,是 Linux 系统最重要的两个抽象概念之一 (另一个是进程) 。另外,VFS 中有个非常重要的结
构体叫file{},这个结构体中包含一个非常重要的成员 叫做file_operation,他通过提供一个统一
的、包罗万象的操作函数集合,来统一规范将来 文件所有可能的操作。某一种文件或设备所支持
的操作都是这个结构体的子集。做 Linux 底 层开发的人对该结构体都应该非常熟悉。
图 1- 1 从应用层的 read( )到底层的 xxx_read( )
图 1- 1 以 read( )为例子,说明了为什么在上层应用中可以对千差万别的设备进行读操作, 头
号功臣就是 file_operation 提供了统一的接口,实际上,VFS 不仅包括 file 结构体,还有 inode 结
构体和 super_block 结构体,正是他们的存在,应用层程序才得以摆脱底层设备的差异细节,独立
于设备之外。
可以看到,内核做了掐头去尾的事情,提供了一个沟通上下的框架,如果你是软件工程师,就
站在用户空间使用下面内核提供的接口,来为你的应用程序服务。如果你是底层驱动 工程师,就
站在操作硬件设备的角度,结合具体设备的操作方式,实现上面内核规定好的各 个该设备可以支
持的接口函数。
有了内核提供的中间层,我们在操作很多不同类型的文件的时候就方便多了,比方说读 取文
件 a.txt 的内容,跟读取触摸屏的坐标数据,读取鼠标的坐标信息等等,用的都是函数 read(),虽
然底层的实现代码也许不一样,但是用户空间的进程并不关心也无需操心,Linux的系统 IO 函数屏
蔽了各类文件的差异,使得我们站在应用编程开发者的角度看下去,产生好像各类文件都一样的感
觉。这就是 Linux 应用编程中一切皆文件的说法的由来。
1.2、各类文件
在 Linux 中,文件总共被分成了 7 种,他们分别是:
1,普通文件 (regular) :存在于外部存储器中,用于存储普通数据。
2, 目录文件 (directory) :用于存放目录项,是文件系统管理的重要文件类型。
3,管道文件 (pipe) :一种用于进程间通信的特殊文件,也称为命名管道 FIFO。
4,套接字文件 (socket) :一种用于网络间通信的特殊文件。
5,链接文件 (link) :用于间接访问另外一个目标文件,相当于 Windows 快捷方式。
6,字符设备文件 (character) :字符设备在应用层的访问接口。
7,块设备文件 (block) :块设备在应用层的访问接口。
2、文件操作
2.1、系统IO (没有缓冲区,效率不高)
2.1.1、函数接口:
2.1.1.1、open()
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能 | 打开一个指定的文件并获得文件描述符,或者创建一个新文件 | ||
头文件 | #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_RDWR:读写方式打开文件 | |||
O_CREAT:如果文件不存在,则创建该文件。 | |||
O_EXCL:如果使用 O_CREAT 选项且文件存在,则返回错误消息。 | |||
O_NOCTTY:如果文件为终端,那么终端不可以作为调用 open()系统调 用的那个进程的控制终端。 | |||
O_TRUNC:如文件已经存在,则删除文件中原有数据。 | |||
O_APPEND:以追加方式打开文件。 | |||
mode | 如果文件被新建,指定其权限为 mode (八进制表示法) | ||
返回值 | 成功 | 大于等于 0 的整数 (即文件描述符) | |
失败 | - 1 | ||
备注 | 无 |
使用系统调用 open( )需要注意的问题有:
1,flags 的各种取值可以用位或的方式叠加起来,比如创建文件的时候需要满足这样的 选项:读写方式打开,不存在要新建,如果存在了则清空他。那么此时指定的 flags 的取值 应该是:O_RDWR | O_CREAT | O_TRUNC。
2,mode 是八进制权限,比如 0644,或者 0755 等。
3,它可以用来打开普通文件、块设备文件、字符设备文件、链接文件和管道文件,但 只能用来创建普通文件,每一种特殊文件的创建都有其特定的其他函数。
4,其返回值就是一个代表这个文件的描述符,是一个非负整数。这个整数将作为以后
任何系统 IO 函数对其操作的句柄,或称入口。
2.1.1.2、close()
功能 | 关闭文件并释放相应资源 | |
头文件 | #include <unistd.h> | |
原型 | int close(int fd); | |
参数 | fd:即将要关闭的文件的描述符 | |
返回值 | 成功 | 0 |
失败 | - 1 | |
备注 | 重复关闭一个已经关闭了的文件或者尚未打开的文件是安全的。 |
2.1.1.3、read()
功能 | 从指定文件中读取数据 | |
头文件 | #include <unistd.h> | |
原型 | ssize_t read(int fd, void *buf, size_t count); | |
参数 | fd:从文件fd 中读数据 | |
buf:指向存放读到的数据的缓冲区 | ||
count:想要从文件 fd 中读取的字节数 | ||
返回值 | 成功 | 实际读到的字节数 |
失败 | - 1 | |
备注 | 实际读到的字节数小于等于 count |
2.1.1.4、write()
功能 | 将数据写入指定的文件 | |
头文件 | #include <unistd h> | |
原型 | ssize_t write(int fd, const void *buf, size_t count); | |
参数 | fd:将数据写入到文件 fd 中 | |
buf:指向即将要写入的数据 | ||
count:要写入的字节数 | ||
返回值 | 成功 | 实际写入的字节数 |
失败 | - 1 | |
备注 | 实际写入的字节数小于等于 count |
2.1.1.5、lseek()
功能 | 调整文件位置偏移量 | ||
头文件 | #include <sys/types.h> #include <unistd.h> | ||
原型 | off_t lseek(int fd, off_t offset, int whence); | ||
参数 | fd:要调整位置偏移量的文件的描述符 | ||
offset:新位置偏移量相对基准点的偏移 | |||
whence:基准点 | SEEK_SET:文件开头处 | ||
SEEK_CUR:当前位置 | |||
SEEK_END:文件末尾处 | |||
返回值 | 成功 | 新文件位置偏移量 | |
失败 | - 1 | ||
备注 | 无 |
2.2、标准IO
2.2.1、fopen()
功能 | 获取指定文件的文件指针 |
头文件 | #include <stdio.h> |
原型 | FILE *fopen(const char *path, const char *mode); |
参数 | path:即将要打开的文件 |
mode | “r” : 以只读方式打开文件,要求文件必须存在。 | |
“r+” : 以读写方式打开文件,要求文件必须存在。 | ||
“w” : 以只写方式打开文件,文件如果不存在将会创建新文件,如果存 在将会将其内容清空。 | ||
“w+” : 以读写方式打开文件,文件如果不存在将会创建新文件,如果存 在将会将其内容清空。 | ||
“a” : 以只写方式打开文件,文件如果不存在将会创建新文件,且文件位 置偏移量被自动定位到文件末尾 (即以追加方式写数据) 。 | ||
“a+” : 以读写方式打开文件,文件如果不存在将会创建新文件,且文件 位置偏移量被自动定位到文件末尾 (即以追加方式写数据) 。 | ||
返回值 | 成功 | 文件指针 |
失败 | NULL | |
备注 | 无 |
2.2.2、fclose()
功能 | 关闭指定的文件并释放其资源 | |
头文件 | #include <stdio.h> | |
原型 | int fclose(FILE *fp); | |
参数 | fp:即将要关闭的文件 | |
返回值 | 成功 | 0 |
失败 | EOF | |
备注 | 无 |
2.2.3、fread()
功能 | 从指定文件读取若干个数据块 | |
头文件 | #include <sys/ioctl.h> | |
原型 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | |
参数 | ptr: 自定义缓冲区指针 | |
size:数据块大小 | ||
nmemb:数据块个数 | ||
stream:即将被读取数据的文件指针 | ||
返回值 | 成功 | 读取的数据块个数,等于 nmemb |
失败 | 读取的数据块个数,小于 nmemb 或等于 0 | |
备注 | 当返回小与nmemb 时,文件 stream 可能已达末尾,或者遇到错误 |
2.2.4、fwrite()
功能 | 将若干块数据写入指定的文件 | |
头文件 | #include <sys/ioctl.h> | |
原型 | size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream); | |
参数 | ptr: 自定义缓冲区指针 | |
size:数据块大小 | ||
nmemb:数据块个数 | ||
stream:即将被写入数据的文件指针 | ||
返回值 | 成功 | 写入的数据块个数,等于 sinmembze |
失败 | 写入的数据块个数,小于 nmemb 或等于 0 | |
备注 | 无 |
2.2.5、fseek()
功能 | 设置指定文件的当前位置偏移量 | ||
头文件 | #include <sys/ioctl.h> | ||
原型 | int fseek(FILE *stream, long offset, int whence); | ||
参数 | stream:需要设置位置偏移量的文件指针 | ||
offset:新位置偏移量相对基准点的偏移 | |||
whence:基准点 | SEEK_SET:文件开头处 | ||
SEEK_CUR:当前位置 | |||
SEEK_END:文件末尾处 | |||
返回值 | 成功 | 0 | |
失败 | - 1 | ||
备注 | 无 |
2.2.6、ftell()
功能 | 获取指定文件的当前位置偏移量 | |
头文件 | #include <sys/ioctl.h> | |
原型 | long ftell(FILE *stream); | |
参数 | stream:需要返回当前文件位置偏移量的文件指针 | |
返回值 | 成功 | 当前文件位置偏移量 |
失败 | - 1 | |
备注 | 无 |
2.2.7、rewind()
功能 | 将指定文件的当前位置偏移量设置到文件开头处 |
头文件 | #include <sys/ioctl.h> |
原型 | void rewind(FILE *stream); |
参数 | stream:需要设置位置偏移量的文件指针 |
返回值 | 无 |
备注 | 该函数的功能是将文件 strean 的位置偏移量置位到文件开头处。 |
3、目录检索
3.1、opendir()和readdir()
现在来看看,对于一个目录而言,我们是怎么处理的。其实操作目录跟标准 IO 函数操 作文件类似,也是先获得“ 目录指针”,然后读取一个个的“目录项”。用到的接口函数是:
功能 | 打开目录以获得目录指针 | |
头文件 | #include <sys/types.h> #include <dirent.h> | |
原型 | DIR *opendir(const char *name); | |
参数 | name: 目录名 | |
返回值 | 成功 | 目录指针 |
失败 | NULL | |
备注 | 无 |
功能 | 读取目录项 | |
头文件 | #include <dirent.h> | |
原型 | struct dirent *readdir(DIR *dirp); | |
参数 | dirp:读出目录项的目录指针 | |
返回值 | 成功 | 目录项指针 |
失败 | NULL | |
备注 | 无 |
从目录中读到的所谓目录项,是一个这样的结构体:
struct dirent
{
ino_t d_ino; // 文件索引号
off_t d_off; // 目录项偏移量
unsigned short d_reclen; // 该目录项大小
unsigned char d_type; // 文件类型
char d_name[256]; // 文件名
};