在linux中,一切IO资源皆文件。自然的,一切设备皆文件。
所以,用户程序中如果要进行IO,首先要open一个FILE,指定FILE_PATH,让内核能够找到对应的IOResource(IOR),并填充一个FileControlBlock,然后返回一个filp给用户。之后,用户可以利用filp作为句柄来操作IOR。
对于用户而言,read和write总是发生在userbuf和IOR之间的。所以我们看到,read和write都会指定fd和userbuf。
struct file{
struct inode *f_inode;
struct file_operations *f_ops;
void *private_data;
struct list_head f_ep_links;
struct list_head f_tfile_llink;
}
这是FILE中,对于LDD而言比较关注的几个成员。
我们看到,FILE关联到INODE,LINUX中,是用INODE表示文件系统中的节点的。可以看出,FILE是依赖于对INODE的引用来实现操作的。
FILE关联到FOPS,linux中,用FOPS作为函数接口,提供了指向TextInstruction的能力。在数据布局中,一个配置好的FOPS,其中的指针都已经指向了合适的函数文本代码。可以看出,FILE是依赖于对FOPS的引用来找到正确的操作函数,并进一步被内核调用的。
FILE关联到PrivData,它是FILE的私有数据的句柄。之所以用关联句柄,而不是内嵌实体,是因为并不能确定PrivData的长度。
FILE内嵌了两个ListHead,说明FILE可以被放到两个不同的链表中。这样,内核就可以通过两个不同的ListRoot找到FILE。除了这两个ListEntry,内核还可以通过FD_Vector来找到这个FILE。FD_Vector是用户的一个ArrayTable,存放32个filp。
struct inode {
dev_t i_rdev;
union{
struct cdev *i_cdev;
struct block_device *i_bdev;
}
};
对于INODE,我们看到,内嵌的i_rdev表示设备号。高12为major,低20为minor。
如果是CDEV,那么INODE关联到一个CDEV,内核通过INODE可以索引到CDEV数据对象上。
如果是BDEV,那么INODE关联到一个BDEV,内核通过INODE可以索引到BDEV数据对象上。
当用户通过
#ls /proc/devices
命令访问文件夹时,内核会找到devices文件的read方法,并调用read。简单而言,这个/proc/devices.fops->read方法会遍历内核中的INODE,并把所有的属性为DEVICES的INODE的DEVID打印出来。
注意,我们心理一定要有这个概念,每一个FILE_PATH,都对应于自己的FOPS。为了清晰显示,我们使用了/proc/devices.fops->read这个表达方式。
我们虽然使用了一条命令,但是这个命令执行的时候,却是执行了多个步骤。
/.fops->open(proc)
/proc.fops->open(devices)
/proc/devices.fops->read()
它需要层层推进,打开每一层的FILE,并找到下一层的INODE,然后打开下一层的FILE。
/proc文件系统是一个伪文件系统,它并不在磁盘上,而是内核动态创建的。
同样的,sysfs也是一个伪文件系统,它也不在磁盘上。
它和kobject紧密相关。所以,sysfs是和LDM配合使用的。
当一个kobject注册到内核中时,注册函数要做两件事,一是把kobject插入kset的链表,二是在sysfs中创建对应的目录。
sysfs是需要挂载的。
所以我们在/etc/fstab中看到,有如下脚本。
#mount -t sysfs sysfs /sys
这个命令,把sysfs挂载到/sys这个目录下面。
mount执行的结果,就是修改了/sys的INODE。使得/sys的INODE指向sysfs的TOPNODE。
这样,之后的操作,就将使用新的INODE和新的FOPS。
/sys/bus/
/sys/devices/
/sys/class/
/sys/firmware/
以上,都是sysfs中必须有的子目录。每一个子目录对应kset,子目录中的文件,又对应kobject。从而形成树状结构。
#cd /sys
#tree
运行上述命令,将得到sysfs的树状结构的文件层次关系。
我们可以发现,其中大部分都是符号链接。实体是放在/sys/devices目录中的。
例如/sys/bus/pci/devices目录中的文件,其实是对/sys/devices中的相应文件的符号链接。
这就引出了Linux Devices Model (LDM)。将设备模型分离为BUS,DEVICE,DRIVER,ATTR。简称BDDA。
struct bus_type{
const char* name;
const char* dev_name;
struct device* dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)();
int (*probe)();
int (*remove)();
}
BUS用bus_type结构体来作为控制块。
BUS关联到一个DEVICE,这个DEVICE作为DEVROOT。
DEVICE可以形成一个链表。BUS通过DEVROOT,就可以找到所有从属于BUS的DEVICE。
其中与驱动相关的几个成员是,name,match,probe,remove。
name是内核用来做字符串比较的文本。
match函数用来对从属于该总线的dev和drv进行匹配处理。
probe函数是BUS被加载时,被内核调用的Callback。
remove函数是BUS被卸载时,被内核调用的Callback。
我们注意到,BUS中没有内嵌listhead,所以BUS并不能添加到链表中。
当bus_register内核API把BUS注册到内核中后,内核只能通过BUS_VectorTable来索引到BUS,并不能通过某些ListRoot来索引到BUS。
struct device_driver{
const char* name;
struct bus_type* bus;
int (*probe)();
int (*remove)();
const struct attribute_group **drv_groups;
const struct of_device_id* of_match_table;
}
struct of_device_id{
char cmpatible[128];
const void* data;
}
DRIVER用device_driver结构体来作为控制块。
可以看到,DRIVER关联到一个BUS结构体。
其中与驱动相关的几个成员是,name,bus,probe,remove。
name是内核用来做字符串比较的文本。
bus是关联的BUS结构体。
probe函数是DRV被加载时,被内核调用的Callback。
remove函数是DRV被卸载时,被内核调用的Callback。
DRVIER关联到一个OF_DEV_ID的TABLE。这可以方便的从DTB中,借助于字符串比较,找到匹配的设备。
(注意,在Linux中,如果命名为Table,则通常是用Array来分配内存的,而如果命令为List,则通常是用链表来分配内存的。我们知道,Array通常用指针来引用。这是linux常见的编程方式。另外就是PointerArray,在linux中,通常是用二级指针来引用。)
我们注意到,DRIVER中没有内嵌listhead,所以DRIVER并不能添加到链表中。当driver_register内核API把DRIVER注册到内核中后,内核只能通过DRV_VectorTable来索引到DRIVER,并不能通过某些ListRoot来索引到DRIVER。
当模块调用driver_register这个内核API来注册某个BUS的DRIVER时,内核会通过DRIVER的关联的BUS,找到BUS中的DEVROOT。然后遍历这些DEVICE,内核这时会调用BUS的match函数,这个callback处理传入的DRIVER和传入的DEVICE的匹配关系。如果匹配,会调用DRIVER的probe函数这个Callback,并把DEV添加到BUS的DEVROOT为首元的链表中。
然后为BUS,DRIVER,DEVICE三者配置指针,使DRIVER能够索引到BUS,使DEVICE能够索引到DRIVER和BUS。
struct device{
const char* init_name;
struct device* parent;
struct kobject kobj;
const struct device_type* type;
void (*release)();
struct bus_type* bus;
struct device_driver* driver;
const struct attribute_group **dev_groups;
void* platform_data;
struct device_node* of_node;
struct list_head devres_head;
struct klist_head knode_class;
}
DEVICE用device结构体来作为控制块。
它内嵌了一个kobject实体,并且内嵌了两个listhead,可以由两个不同的listroot进行查找。
它关联了一个DEV,指向它的父设备。
它关联了一个DEV_TYPE,用来描述设备的类型。
几个关键的成员,init_name,bus,driver,release,of_node,platform_data。
它关联了一个BUS和一个DRIVER,从而使DEVICE在逻辑上从属于BUS和DRIVER。通过关联的指针,可以索引到对应的BUS和对应的DRIVER。
release函数,是DEV的引用数为0时,被内核调用的Callback。
of_node关联到一个OF_NODE结构体,它可以方便的从DTB中,找到匹配的设备。
我们注意到,DEV中内嵌listhead,所以DEV能添加到链表中。当device_register内核API把DEV注册到内核中后,内核除了能通过DEV_VectorTable来索引到DRIVER,还能通过某些ListRoot来索引到DEV。
当模块调用device_register这个内核API来注册某个BUS的DEV时,内核会通过DRV_VectorTable,遍历注册到内核中的DRIVER,而DRIVER会找到关联的BUS,并调用BUS的match函数,这个callback处理传入的DRIVER和传入的DEVICE的匹配关系。如果匹配,会调用DRIVER的probe函数这个Callback,并把DEV添加到BUS的DEVROOT为首元的链表中。
然后为BUS,DRIVER,DEVICE三者配置指针,使DRIVER能够索引到BUS,使DEVICE能够索引到DRIVER和BUS。
struct atrribute{
const char* name;
umode_t mode;
}
struct bus_atrribute{
struct atrribute attr;
ssize_t (*show)();
ssize_t (*store)();
}
struct device_atrribute{
struct atrribute attr;
ssize_t (*show)();
ssize_t (*store)();
}
struct driver_atrribute{
struct atrribute attr;
ssize_t (*show)();
ssize_t (*store)();
}
ATTR用attribute结构体来作为控制块。BDD对ATTR进行了扩展,内嵌一个ATTR实体后,形成了BUSATTR,DEVATTR和DRVATTR三种结构体。
sysfs中的目录,对应于BDD,而目录中的文件,则对应于ATTR。
这样,当用户通过sysfs访问到ATTR时,read操作将调用ATTR.show这个Callback。write操作将调用ATTR.store这个Callback。
来看一个简单的例子。我们定义了三个模块。vbus,vdrv,vdev。
static struct bus_type vbus = {
.name = "vbus",
.match = vbus_match,
}
static int __init vbus_init(void)
{
int ret;
ret = bus_register(&vbus);
return ret;
}
static struct device_driver vdrv = {
.name = "vdrv",
.bus = &vbus,
}
static int __init vdrv_init(void)
{
int ret;
ret = driver_register(&vdrv);
return ret;
}
static struct device vdev = {
.init_name = "vdev",
.bus = &vbus,
.release = vdev_release,
}
static int __init vdev_init(void)
{
int ret;
ret = device_register(&vdev);
return ret;
}
static void vdev_release(struct device *dev)
{
return;
}
从中可以看到,BUS,DEV,DRV相互之间的关联关系。
再来看一个更具有代表性的例子。
仍然是定义了三个模块。packt_bus,packt_bus_drv,packt_bus_dev
struct bus_type packt_bus_type = {
.name = "packt",
.match = packt_device_match,
.probe = packt_device_probe,
.remove = packt_device_remove,
.shutdown = packt_device_shutdown,
}
struct device packt_bus = {
.parent = NULL,
.release = packt_bus_release,
}
static int __init packt_init(void)
{
int status;
status = bus_register(&packt_bus_type);
if(status < 0)
goto err0;
status = class_register(&packt_master_class);
if(status < 0)
goto err1;
device_register(&packt_bus);
return 0;
err1:
bus_unregister(&packt_bus_type);
err0:
return status;
}
module_init(packt_init);
EXPORT_SYMBOL(packt_bus );
EXPORT_SYMBOL(packt_bus_type );
在这个模块中,我们定义了一个BUS的实体,和一个DEV的实体,并静态化处理。
在init函数被调用时,将BUS和DEV注册到内核中。
错误处理代码,用的是堆栈式回滚处理方式。
当device_register被调用时,内核会遍历DRIVER,所以这个模块的依赖模块,是下面这个DRV模块。
struct packt_device {
struct device dev;
long price;
}
struct packt_driver{
struct device_driver driver;
int (*probe)(struct packt_device * packt);
int (*remove)(struct packt_device * packt);
}
static packt_driver packt_drv = {
.driver = {},
}
static packt_device packt_dev = {
.dev = {},
}
int packt_register_driver(struct packt_driver *drv)
{
int ret;
drv.driver.bus = & packt_bus_type;
ret = driver_register(&drv.driver);
return ret;
}
int packt_device_register(struct packt_device * packt)
{
int ret;
packt.dev.parent = &parent_bus;
packt.dev.bus = &packt_bus_type;
ret = device_register(&packt.dev);
return ret;
}
static int __init packt_driver_init(void)
{
int status;
status = packt_register_driver(&packt_drv);
if(status < 0)
goto err0;
packt_device_register(&packt_dev );
return 0;
err0:
return status;
}
module_init(packt_driver_init);
EXPORT_SYMBOL(packt_register_driver);
EXPORT_SYMBOL(packt_device_register);
这个例子中,对BUS,DEV,DRV进行了扩展,衍生了一些数据成员,用来进行具体的驱动任务。