根据设备的共性大致分为3类
- 字符设备,以字节为单位的I/O传输,比如鼠标、键盘、触摸屏(CXL.mem也是以字节为单位传输,但是不是字符数据流)。
- 块设备,以块为传输单位,比如SSD、磁盘。
- 网络设备,
字符设备的抽象
Linux如何抽象和描述这些设备的呢?这里主要讨论字符设备include\linux\cdev.h
:
struct cdev {
struct kobject kobj; //该驱动所属的kobject,可以通过sysfs接口将其挂载到所需要的sysfs节点,注意需要驱动开发人员手动去挂载,本身字符驱动不会进行挂载只会对kobj初始化,需要相应具体设备驱动进行挂载。
struct module *owner; //字符设备驱动所在的内核模块对象指针。
const struct file_operations *ops; //文件操作 openc、read、write等,和应用程序交互的枢纽。
struct list_head list; //用于将字符设备串成链表。
dev_t dev; //设备号(主设备和次设备组成)
unsigned int count; //同属主设备号的次设备号个数
}
以下是一个简单字符设备demo只包含字符驱动的框架:
内核中创建字符驱动
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#define DEMO_NAME "my_demo_dev" //设备名
static dev_t dev; //设备号
static struct cdev *demo_cdev; //静态全局变量的方式创建字符设备
static signed count = 1;
//打开设备
static int demodrv_open(struct inode *inode, struct file *file)
{
int major = MAJOR(inode->i_rdev);
int minor = MINOR(inode->i_rdev);
printk("%s: major=%d, minor=%d\n", __func__, major, minor);
return 0;
}
// 释放设备
static int demodrv_release(struct inode *inode, struct file *file)
{
return 0;
}
//读取设备
static ssize_t
demodrv_read(struct file *file, char __user *buf, size_t lbuf, loff_t *ppos)
{
printk("%s enter\n", __func__);
return 0;
}
//写入设备
static ssize_t
demodrv_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("%s enter\n", __func__);
return 0;
}
//定义操作集合
static const struct file_operations demodrv_fops = {
.owner = THIS_MODULE,
.open = demodrv_open,
.release = demodrv_release,
.read = demodrv_read,
.write = demodrv_write
};
static int __init simple_char_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME); //自动分配主设备号,避免重复
if (ret) {
printk("failed to allocate char device region");
return ret;
}
demo_cdev = cdev_alloc(); //动态创建字符设备
if (!demo_cdev) {
printk("cdev_alloc failed\n");
goto unregister_chrdev;
}
cdev_init(demo_cdev, &demodrv_fops); //初始化cdve结构体,建立设备与驱动操作方法集file_operations之间的链接关系
ret = cdev_add(demo_cdev, dev, count); //将字符设备添加到系统(注册字符设备)[字符设备结构体,设备号,多少次设备号]
if (ret) {
printk("cdev_add failed\n");
goto cdev_fail;
}
printk("succeeded register char device: %s\n", DEMO_NAME);
printk("Major number = %d, minor number = %d\n",
MAJOR(dev), MINOR(dev));
return 0;
cdev_fail:
cdev_del(demo_cdev); //从系统中删除这个结构体
unregister_chrdev:
unregister_chrdev_region(dev, count); //释放主设备号
return ret;
}
static void __exit simple_char_exit(void)
{
printk("removing device\n");
if (demo_cdev)
cdev_del(demo_cdev);
unregister_chrdev_region(dev, count);
}
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_AUTHOR("kylin");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("simpe character device");
编译这个内核模块:
BASEINCLUDE ?= /lib/modules/`uname -r`/build
mydemo-objs := simple_char.o
obj-m := mydemo.o
all :
$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
clean:
$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
rm -f *.ko;
make
编译后生成mydemo.ko
, 然后加载这个模块
sudo insmod mydemo.ko
加载内核模块后可以在dmesg
命令输出看到simple_char_init
函数成功初始化的输出。/proc/devices
可以看到所有的设备名称和设备编号。
设备节点
内核模块加载后还需要在/dev/
目录下生成对应的节点,249是刚刚加载这个内核模块时分配的主设备号。
sudo mknod /dev/demo_drv c 249 0
设备节点属于特殊的文件,也叫设备文件。连接内核空间驱动和用户空间应用程序的桥梁,用户操作硬件设备变成了操作文件。设备节点的生成方式有mknod
手动生成,也可以udev
机制动态生成。ls -l
可以查看/dev/
目录的情况,其中c
表示字符设备,b
表示块设备。
用户空间对字符设备的调用
内核完成对这个设备加载后,用户空间操作这个字符驱动设备
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/demo_drv"
int main()
{
char buffer[64];
int fd;
fd = open(DEMO_DEV_NAME, O_RDONLY); //打开设备[设备文件名刚刚手动挂载的那个名字,文件打开属性]
if (fd < 0) {
printf("open device %s failded\n", DEMO_DEV_NAME);
return -1;
}
read(fd, buffer, 64); //调用读函数
close(fd); //关闭设备
return 0;
}
这里read的调用是来自之前提到的file_operations
,这个方法集结构体是由cdev_init
方法和用户空间建立联系的。执行成功返回一个文件描述符(文件句柄)。应用程序的open
方法执行时会通过系统调用进入内核空间,内核空间的虚拟文件系统层经过复杂的转换,最后会调用设备驱动的file_operations
方法集里的open
方法。
misc机制
misc device被称为杂项设备,Linux内核把一些不符合预先确定的字符设备划分为杂项设备,这类设备的主设备号是10,次设备号动态分配,Linux内核使用struct miscdevice数据结构描述这类设备。使用misc机制创建驱动的过程和上面差不多,其中misc的设备文件可以自己建好,和设备名一样(之前手动建的可以换名字)
还有IO阻塞,异步通信、IO复用等,但是和CXL.mem应该关系不大,所以就简单看看,不写笔记了。
Memory Pooling With CXL
这篇和22年他们发的分解内存相似度极高。程序通过malloc
申请内存,这个c语言库函数有个阈值是128k。大于128k调用mmap
,小于调用brk
(不过不同glibc的这个值可能不太一样)。如果申请的是比较大块的内存(超过 128K)时,会调用mmap
在虚拟内存空间中的文件映射与匿名映射区创建出一块VMA
内存区域,用struct anon_vma
结构表示。在这篇论文里mmap
的映射是文件映射。就是说将进程虚拟内存空间中某一段虚拟内存区域与磁盘中某个文件的某段区域进行映射。通过mmap
只是给程序的某个进程分配了虚拟内存,还没完。这些虚拟地址和物理地址的关系是怎么建立的?以前通过mmap
映射的是磁盘上的一个文件,那么就需要通过参数fd
来指定要映射文件的描述符(file descriptor)。现在是去访问一个设备。一般来说内核访问一个设备需要设备驱动程序进行一系列操作包括在/dev
目录下创建一个字符设备文件或块设备文件。/dev
下的PCIe设备文件抽象了应用程序与底层硬件的交互,通过文件操作的统一接口完成对PCIe设备的访问。而这篇文章里DIRECTCXL驱动程序将基址寄存器和HDM映射到主机的保留系统内存空间中。Host-Managed Device Memory技术让设备内存访问对主机来说就像访问系统内存一样,通过加载/存储指令直接操作。对主机来说以前PM的物理地址只是排在DRAM后面的一段线性地址,那现在又如何看CXL设备的内存?从论文的描述来看CXL内存并不对主机透明,而是需要多少就现成分多少,DIRECTCXL驱动程序就创建多少/dev/xx
。这篇文章里CXL内存设备的虚拟地址地址和物理地址的映射更像传统段式内存。如下图展示了程序该怎么使用CXL设备内存的整个流程。无论是NVM还是CXL作为物理内存块,它们首先都是由namespace定义一套内存的抽象(这个过程不是我们讨论的范围)。只是以前PM将namespace通过热插拔设置为node,而现在是设置为如下图的段式内存。另一个问题为什么放在/dev/directcxl
之类的地方?因为他实现CXL的方式是基于PCIe的,是个设备。