目录
0.操作系统中的缺页
1.mmap文件映射
2.进程的虚拟地址空间
3.Linux中的字符设备驱动
4.struct file_operations
5.mmap的调用流程分析
6.实验
0.操作系统中的缺页
缺页是引入了虚拟内存后的一个概念。操作系统启动后,在内存中维护着一个虚拟地址表,进程需要的虚
拟地址在虚拟地址表中记录。一个程序被加载运行时,只是加载了很少的一部分到内存,另外一部分在需
要时再从磁盘载入。被加载到内存的部分标识为“驻留”,而未被加载到内存的部分标为“未驻留”。操作系
统根据需要读取虚拟地址表,如果读到虚拟地址表中记录的地址被标为“未驻入”,表示这部分地址记录的
程序代码未被加载到内存,需要从磁盘读入,则这种情况就表示"缺页"。这个时候,操作系统触发一个
“缺页”的硬件陷井,系统从磁盘换入这部分未“驻留”的代码。
引入了分页机制(也就有了缺页机制),则系统只需要加载程序的部分代码到内存,就可以创建进
程运行, 需要程序的另一部分时再从磁盘载入并运行,从而允许比内存大很多的程序同时在内存运行。
1.mmap文件映射
mmap可以将一个文件映射到进程的地址空间,建立文件磁盘地址和进程虚拟地址的映射关系,
这样进程就可以通过读取相应虚拟地址而直接读取相应文件中的内容了,这样映射的最大好处
是进程可以直接访问内存,避免了频繁地使用read和write等文件系统的系统调用.
需要注意的是mmap并不分配物理内存,它所做的最重要的工作就是为进程映射区的虚拟地址
建立页表项,从图中可以看出进程的虚拟地址空间是由多个虚拟内存区域构成的,如图txt数据
段,初识数据段,bss数据段,堆和栈都是一个个独立的虚拟内存区域,而为内存映射服务的
地址空间处在堆和栈之间的空余部分.

2.进程的虚拟地址空间
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域的数据结构,由于每个
虚拟内存区域的功能和内部机制都不尽相同,因此一个进程会使用多个vm_area_struct
结构体的对象来分别表示不同类型的内存区域.
也就是我们在本文第一小节中看到的text数据段,初识数据段等等.
每个vm_area_struct结构体的对象都对应这个虚拟地址空间上一段连续的地址,它们之间通过
链表或者树形结构来连接,方便进程进行快速的查找和访问.这里我们可以看到vm_area_struct
结构中的一些字段:
vm_end --虚拟内存区域的结束地址
vm_start--虚拟内存区域的开始地址
vm_flags--该虚拟内存区域的标志位
vm_inode--如果虚拟内存区域映射的是一个磁盘文件或者设备文件的话,那么vm_inode就指向
该文件的inode索引节点
vm_ops --是一个指向const struct vm_operations_struct结构体对象的指针,在
struct vm_operations_struct结构体中定义了与该虚拟内存区相关的接口,
其中包括open,close等操作,每个虚拟内存区域都必须在vm_operations_struc
这个结构中实现这些操作
一个进程的全部虚拟空间是由mm_struct结构体来管理的,里面包括各种信息:
pdg --页目录表
mmap--指向虚拟内存区链表的指针

3.Linux中的字符设备驱动
所有的设备在linux中都是以设备文件的形式存在,设备文件允许应用程序通过标准输入输出
系统调用与驱动程序进行交互,既然它们也是文件,就也可以通过mmap进行映射,这是一种操作
设备的方法,有关设备和驱动是一块很大的领域,感兴趣可做另外的研究.
今天以要用的字符设备驱动为例,简单地介绍一下它的基本内容.
在用户程序看来,操作一个设备就是对设备文件的读写,具体的实现过程则是由相应的驱动程序
来完成的.
如图所示,方框里就是一个设备驱动中的主要内容,其中少不了对模块的加载和卸载函数(它们
主要完成设备的初始化和删除),它使用struct cdev结构体来抽象描述一个字符设备,一个
struct cdev结构体由一个dev_t的设备号唯一指定,设备号分为主设备号和次设备号,主设备号
用来表明设备类型,次设备号用来表明其编号,struct cdev结构体还有一个file_operation结构
体,这个结构体是linux文件系统中一个非常重要的结构体,linux中的vfs可以将不同类型的
文件系统进行统一管理并为用户提供一个统一的接口,就是通过file_operation结构体来实现.
在设备文件中,主要用来存储驱动模块提供的对设备进行操作的各种函数.
对于普通文件的read,write,驱动程序需要将其转化为对应的对设备的操作,
就是通过该结构体完成的.包括了很多钩子函数,包括read,release,mmap等.
read就是进程在读设备时要做的,
release是调用close时要做的工作,用来释放一些系统资源.
最后就是mmap.
不同的文件有自己定义的mmap钩子,比如在ext文件系统中,对应的generic_file_mmap
的钩子函数,今天我们需要做的工作就是为一个虚拟字符设备编写其驱动模块,在驱动中
完成设备空间与内核空间到用户空间的内存映射.

4.struct file_operations
D:\005-代码\001-开源项目源码\004-内核源码\linux-5.8.13\linux-5.8.13\include\linux\fs.h
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
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 (*iopoll)(struct kiocb *kiocb, bool spin);
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);
int (*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_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_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;
5.mmap的调用流程分析
当用户在程序中调用mmap这个功能之后,用户需要在图中的函数,
首先一个用户进程在调用mmap这个系统调用后,系统会为其在当前进程的虚拟地址空间中
寻找一段连续的内存地址,通过遍历vm_area_struct组成的链表来实现的.
当找到一个合适的地址区间之后,为其建立一个vm_area_struct结构,完成这个,
进程就拥有了一个专门用于mmap映射的虚拟内存区.
但是这样还不够,因为我们在进程的页表中这个区域中的线性地址都没有对应的物理页框,
接着系统会调用内核函数file->f_op->mmap,它会帮助vm_area_struct结构建立对应的页表项.
建立页表项有两种方法:
1.remap_pfn_range();
2.fault().
前者是要求物理地址连续,是在内核函数mmap(file->f_op->mmap)中要完成的函数,它在被调用时
一次性地为vm_area_struct结构中的这些线性地址建立页表项,所以会要求这些页表项所要映射
的物理地址应该是连续的.
后者是进程访问到映射空间的虚拟地址时发现虚拟地址的
页表项为空引起缺页时才被调用,更适合非malloc分配的不连续的物理地址进行映射.

6.实验


问题:
开始的时候是能打出来缺页处理函数的日志的,
之后再实验就无法打出缺页处理函数的日志了,
这个是为什么呢?
是因为没有缺页发生吗?
是不是因为我的操作已经将内容放到物理内存中了呢?
---Yep,已经被加载到内存中了,直接访问就可以了,不会产生缺页异常了.