轻松Linux-6.基础IO
祝抗战胜利80周年
本章节的内核源码参考为:linux-5.0-rc3
1.概述
我们学习C语言时,都知道文件操作的相关函数,例如:fopen, fwrite等等, 其实这些都是C语言标准库封装系统调用接口提供的函数,并且在系统调用接口之上,C语言标准库还封装了一套缓冲区,来给C函数使用,但系统调用接口本身并没有封装对应的缓冲区。我们常听一句话就是,在“Linux中一切皆文件”,我们怎么来理解这个一切接文件呢?
狭义上的文件就是:磁盘中的数据,对磁盘中的数据进行的操作,就是IO操作。
广义上的文件就是:Linux将各种设备(网卡,磁盘,显示器等等)通过相应的驱动抽象成各种文件。
那么为什么Linux要这样操作,这样操作又有什么好处呢?
Linux将各种设备抽象成文件,这样就可以通过一套统一的系统接口API来对所有设备进行IO操作、来调取系统中的大部分资源。
文件是什么?
文件就是文件内容和文件属性的结合,即 文件 = 属性(元数据) + 内容。
即使是0KB的文件,也是要占用磁盘数据的,因为系统要存储文件的属性。
对文件属性的操作和对文件内容的操作可以归结为对文件操作。
2.系统文件IO
2.1系统文件IO接口
在了解Linux底层的文件操作代码之前,我们不妨先了解系统的文件IO接口,有open、write、lseek、close等等,与C语言标准库都非常相似,这里我们介绍open函数,其他函数类比C文件操作。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnlt.h>int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);pathname: 文件的路径+文件名
flags: 打开文件的模式(一下都是宏, 可以使用符号|来选择, 如O_WRONLY | O_CREAT | O_APPEND)
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR: 读写打开
以上三个参数必须填一个
O_CREAT: 若文件不存在, 就创建, 再通过mode指定新文件的访问权限(不填则用系统默认参数)
O_APPEND: 以追加写的方式打开
O_TRUNC: 打开时清空文件内容mode_t可以查阅Linux的man手册返回值:
成功时, 返回一个新的文件描述符(后面会介绍)
失败时, 返回-1
2.2文件描述符
从前面open函数的介绍我们可以知道文件描述符是一个整数,其中在Linux中会默认打开0、1和2 这3个文件描述符,这里0、1、2文件描述符分别代表stdin(标准输入)、stdout(标准输出)、stderr(标准错误),它们一般代表的设备是:键盘、显示屏、显示屏。
前面我们说到C文件封装了缓冲区(可以提高读写效率),但是这里的stderr是没有对应的缓冲区的,这样可以更加直接的输出错误信息。
文件描述符的分配规则:
文件描述符是从0开始分配的,在分配新的文件描述符时,系统会选择最前的未被分配的文件描述符,也就是说,在下一次分配前,我们关闭了文件描述符1,因为0默认已经打开,1被关闭了,分配时会将1分配出去,同时这也意味着,文件描述符1不再指向stdout,而是指向新指定的文件。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnlt.h>int main()
{close(0);int fd = open("1.txt", O_RDONLY | O_CREAT);if(fd < 0){printf("open error!");return 1;}printf("%d\n", fd);close(fd);return 0;
}
例如上述代码,因为关闭了0号描述符,在下一次打开文件时分配了0,在输出时,就输出0。
在我们打开文件的时候,操作系统也会相应的创建数据结构来描述管理文件,文件描述符就是其中的一个属性。操作系统为了将进程和文件更好的关联起来,在PCB中增加了一个file* 指针,来指向创建的file_struct。
这里的fd_array就是文件描述符表,其中这里文件描述符也是有限,也就是文件描述符资源也是会消耗完的,这就会引出另一种问题,文件描述泄露(这里不讲)。
每个打开的文件,在内核中都有对应的file对象来储存文件的属性已经inode元信息。
所以可以认为每个文件描述符就是文件的指针,每个进程都可以通过文件描述符来找到对应的文件。
2.3重定向
什么是重定向?其实上面的例子已经涉及到了,它的本质就是让文件描述符指向其它文件,比如关闭文件描述符1(stdout),再打开新文件,这就是一种重定向。如图:
当然,我们要想重定向还不需要这么麻烦,系统提供相应的接口dup和dup2。
#include <unistd.h>int dup(int oldfd);
int dup2(int oldfd, int newfd);这里dup的功能是将目前未被占用的最小的文件描述符,指向oldfd原指向的文件
dup2则是,让newfd(你要用来替换的文件描述符)来指向,oldfd指向的文件,如果newfd已经
打开,会先将其关闭
3."一切皆文件"
Linux中显示器、硬盘、显卡、网络socket等等都被抽象成了文件,这样它们就可以用一套统一的接口read、write来读取写入,也就是用一套API,就可以最大限度的读取系统资源。
几乎所有的状态读取都可以用read来操作,几乎所有的状态改变都可以用write来实现。
在file_struct中我们可以看到有一个*f_op这个指针,它指向了file_operations这个结构体,这个结构体就是把系统调用和硬件驱动相关联的,可以看到这个结构体除了第一个成员函数外,都是函数指针(系统调用),系统只要读取这里的函数指针,然后将控制权交给函数即可。
struct file_operations {// 表示这个模块的所属者struct module *owner;// llseek改变当前文件的读写位置,返回一个新的正整数loff_t (*llseek) (struct file *, loff_t, int);// 用于读取设备上的数据,如果第一个参数传空指针,会返回 -EINVAL错误,成功则返回一个非负的整数表示成功读取的字节数(对应平台signed size整数)ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);// 用于向设备发送数据,第一个参数同上,成功则返回一个非负的正整数,代表成功发送的字节数ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);// 初始化一个异步读,在函数返回前一般不会结束ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);// 初始化一个异步写ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);int (*iterate_shared) (struct file *, struct dir_context *);// 它用于读取文件目录,只能用于 文件系统__poll_t (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);// 用于将设备的内存映射到进程地址空间中,如果这个方法是NULL,则会返回 -ENODEVint (*mmap) (struct file *, struct vm_area_struct *);unsigned long mmap_supported_flags;// 打开一个文件int (*open) (struct inode *, struct file *);// 在进程关闭它对应的文件描述符拷贝时调用,刷新数据int (*flush) (struct file *, fl_owner_t id);// 关闭时释放文件结构int (*release) (struct inode *, struct file *);// 用户用来刷新一切挂着的数据int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endifssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,struct file *file_out, loff_t pos_out,loff_t len, unsigned int remap_flags);int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
几乎每个设备都可以有自己的read、write函数,尽管操作的方法可能大不相同,但是通过file_operations中的各种函数回调,即可用file的方式来读取尽可能多的资源,这就可以理解为Linux中“一切皆文件”。
4.缓冲区
缓冲区,其实就是就在内存上预留出一部分空间,用于缓冲输入和输出的数据,根据需求又可以分为输入和输出缓冲区。
在读写文件时,如果没有缓冲区,那么每次进行文件的读写都要调用一次系统调用,而系统调用的消耗是很大的,需要CPU从用户态切换到内核态,从而实现CPU上下文的切换,频繁的访问文件就会对程序的执行效率有非常大的影响。
为了提高效率,设计了缓冲区,这样我们就可以一次从磁盘中读入大量数据到缓冲区,或从缓冲区写大量数据到磁盘,从而减少使用系统调用的次数,加之系统对缓冲区的读写速度远高于磁盘,这样既减少了磁盘读取速度,又提高了系统读写速度,就自然提高了效率。
4.1 缓冲策略
缓冲策略一般有三种:
行缓冲:此模式下,在输出或输入时遇到换行符时,标准IO库会执行系统调用刷新数据。当操作涉及下一个终端(例如标准输出、标准输入)时,进行刷新。其他当行缓冲区满时刷新,默认的缓冲区大小为1024。
全缓冲:当缓冲区满时,进行刷新。通常用于磁盘操作。
无缓冲:直接执行系统调用
注:stderr、系统调用一般无缓冲区。