Linux设备模型 — 总线、设备、驱动程序和类(机制理解)

文章的例子和实验使用《LDD3》所配的lddbus模块(稍作修改)。

提示:在学习这部分内容是一定要分析所有介绍的源代码,知道他们与上一部分内容(kobject、kset、attribute等等)的关系,最好要分析一个实际的“flatform device”设备,不然会只学到表象,到后面会不知所云的。

总线

总线是处理器和一个或多个设备之间的通道,在设备模型中, 所有的设备都通过总线相连, 甚至是内部的虚拟"platform"总线。总线可以相互插入。设备模型展示了总线和它们所控制的设备之间的实际连接。在 Linux 设备模型中, 总线由 bus_type 结构表示, 定义在<linux/device.h>:

structbus_type{constchar*name;/*总线类型名称*/structmodule*owner;/*指向模块的指针(如果有), 此模块负责操作这个总线*/structksetsubsys;/*与该总线相关的子系统*/structksetdrivers;/*总线驱动程序的kset*/structksetdevices;/* 挂在该总线的所有设备的kset*/structklistklist_devices;/*与该总线相关的驱动程序链表*/structklistklist_drivers;/*挂接在该总线的设备链表*/structblocking_notifier_head bus_notifier;structbus_attribute*bus_attrs;/*总线属性*/structdevice_attribute*dev_attrs;/*设备属性,指向为每个加入总线的设备建立的默认属性链表*/structdriver_attribute*drv_attrs;/*驱动程序属性*/structbus_attribute drivers_autoprobe_attr;/*驱动自动探测属性*/structbus_attribute drivers_probe_attr;/*驱动探测属性*/int(*match)(structdevice*dev,structdevice_driver*drv);int(*uevent)(structdevice*dev,char**envp,intnum_envp,char*buffer,intbuffer_size);int(*probe)(structdevice*dev);int(*remove)(structdevice*dev);void(*shutdown)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_t state);int(*suspend_late)(structdevice*dev,pm_message_t state);int(*resume_early)(structdevice*dev);nt(*resume)(structdevice*dev);/*处理热插拔、电源管理、探测和移除等事件的方法*/unsignedintdrivers_autoprobe:1;};

在更新的内核里,这个结构体变得更简洁了,隐藏了无需驱动编程人员知道的一些成员:

/*in Linux 2.6.26.5*/

structbus_type{constchar*name;structbus_attribute*bus_attrs;structdevice_attribute*dev_attrs;structdriver_attribute*drv_attrs;int(*match)(structdevice*dev,structdevice_driver*drv);int(*uevent)(structdevice*dev,structkobj_uevent_env*env);int(*probe)(structdevice*dev);int(*remove)(structdevice*dev);void(*shutdown)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_t state);int(*suspend_late)(structdevice*dev,pm_message_t state);int(*resume_early)(structdevice*dev);int(*resume)(structdevice*dev);structbus_type_private*p;};structbus_type_private{structkset subsys;structkset*drivers_kset;structkset*devices_kset;structklist klist_devices;structklist klist_drivers;structblocking_notifier_head bus_notifier;unsignedintdrivers_autoprobe:1;structbus_type*bus;};

总线的注册和删除

总线的主要注册步骤:

(1)申明和初始化 bus_type 结构体。只有很少的 bus_type 成员需要初始化,大部分都由设备模型核心控制。但必须为总线指定名字及一些必要的方法。例如:

structbus_type ldd_bus_type={.name="ldd",.match=ldd_match,.uevent=ldd_uevent,};

(2)调用bus_register函数注册总线。

intbus_register(structbus_type*bus)

调用可能失败, 所以必须始终检查返回值。若成功,新的总线子系统将被添加进系统,并可在 sysfs的 /sys/bus 下看到。之后可以向总线添加设备。例如:

ret=bus_register(&ldd_bus_type);if(ret)returnret;

当必须从系统中删除一个总线时, 调用:

voidbus_unregister(structbus_type*bus);

总线方法

在 bus_type 结构中定义了许多方法,它们允许总线核心作为设备核心和单独的驱动程序之间提供服务的中介,主要介绍以下两个方法:

int(*match)(structdevice*dev,structdevice_driver*drv);/*当一个新设备或者驱动被添加到这个总线时,这个方法会被调用一次或多次,若指定的驱动程序能够处理指定的设备,则返回非零值。必须在总线层使用这个函数, 因为那里存在正确的逻辑,核心内核不知道如何为每个总线类型匹配设备和驱动程序*/int(*uevent)(structdevice*dev,char**envp,intnum_envp,char*buffer,intbuffer_size);/*在为用户空间产生热插拔事件之前,这个方法允许总线添加环境变量(参数和 kset 的uevent方法相同)*/

lddbus的match和uevent方法:

staticintldd_match(structdevice*dev,structdevice_driver*driver){return!strncmp(dev->bus_id,driver->name,strlen(driver->name));}/*仅简单比较驱动和设备的名字*//*当涉及实际硬件时, match 函数常常对设备提供的硬件 ID 和驱动所支持的 ID 做比较*/staticintldd_uevent(structdevice*dev,char**envp,intnum_envp,char*buffer,intbuffer_size){envp[0]=buffer;if(snprintf(buffer,buffer_size,"LDDBUS_VERSION=%s",Version)>=buffer_size)return-ENOMEM;envp[1]=NULL;return0;}/*在环境变量中加入 lddbus 源码的当前版本号*/

对设备和驱动的迭代

若要编写总线层代码, 可能不得不对所有已经注册到总线的设备或驱动进行一些操作,这可能需要仔细研究嵌入到 bus_type 结构中的其他数据结构, 但最好使用内核提供的辅助函数:

intbus_for_each_dev(structbus_type*bus,structdevice*start,void*data,int(*fn)(structdevice*,void*));intbus_for_each_drv(structbus_type*bus,structdevice_driver*start,void*data,int(*fn)(structdevice_driver*,void*));/*这两个函数迭代总线上的每个设备或驱动程序, 将关联的 device 或 device_driver 传递给 fn, 同时传递 data 值。若 start 为 NULL, 则从第一个设备开始; 否则从 start 之后的第一个设备开始。若 fn 返回非零值, 迭代停止并且那个值从 bus_for_each_dev 或bus_for_each_drv 返回。*/

总线属性

几乎 Linux 设备模型中的每一层都提供添加属性的函数, 总线层也不例外。bus_attribute 类型定义在<linux/device.h>如下:

structbus_attribute{structattributeattr;ssize_t(*show)(structbus_type*,char*buf);ssize_t(*store)(structbus_type*,constchar*buf,size_tcount);};

可以看出structbus_attribute 和structattribute 很相似,其实大部分在 kobject 级上的设备模型层都是以这种方式工作。

内核提供了一个宏在编译时创建和初始化 bus_attribute 结构:

BUS_ATTR(_name,_mode,_show,_store)/*这个宏声明一个结构, 将 bus_attr_ 作为给定 _name 的前缀来创建总线的真正名称*//*总线的属性必须显式调用 bus_create_file 来创建:*/intbus_create_file(structbus_type*bus,structbus_attribute*attr);/*删除总线的属性调用:*/voidbus_remove_file(structbus_type*bus,structbus_attribute*attr);

例如创建一个包含源码版本号简单属性文件方法如下:

staticssize_t show_bus_version(structbus_type*bus,char*buf){returnsnprintf(buf,PAGE_SIZE,"%s\n",Version);}staticBUS_ATTR(version,S_IRUGO,show_bus_version,NULL);/*在模块加载时创建属性文件:*/if(bus_create_file(&ldd_bus_type,&bus_attr_version))printk(KERN_NOTICE"Unable to create version attribute\n");/*这个调用创建一个包含 lddbus 代码的版本号的属性文件(/sys/bus/ldd/version)*/


设备

在最底层, Linux 系统中的每个设备由一个 struct device 代表:

structdevice{structklistklist_children;structklist_nodeknode_parent;/* node in sibling list */structklist_nodeknode_driver;structklist_nodeknode_bus;structdevice*parent;/* 设备的 "父" 设备,该设备所属的设备,通常一个父设备是某种总线或者主控制器. 如果 parent 是 NULL, 则该设备是顶层设备,较少见 */structkobject kobj;/*代表该设备并将其连接到结构体系中的 kobject; 注意:作为通用的规则, device->kobj->parent 应等于 device->parent->kobj*/charbus_id[BUS_ID_SIZE];/*在总线上唯一标识该设备的字符串;例如: PCI 设备使用标准的 PCI ID 格式, 包含:域, 总线, 设备, 和功能号.*/structdevice_type*type;unsignedis_registered:1;unsigneduevent_suppress:1;structdevice_attribute uevent_attr;structdevice_attribute*devt_attr;structsemaphoresem;/* semaphore to synchronize calls to its driver. */structbus_type*bus;/*标识该设备连接在何种类型的总线上*/structdevice_driver*driver;/*管理该设备的驱动程序*/void*driver_data;/*该设备驱动使用的私有数据成员*/void*platform_data;/* Platform specific data, devicecore doesn't touch it */structdev_pm_infopower;#ifdefCONFIG_NUMAintnuma_node;/* NUMA node this device is close to */#endifu64*dma_mask;/* dma mask (if dma'able device) */u64coherent_dma_mask;/* Like dma_mask, but for alloc_coherent mappings as not all hardware supports 64 bit addresses for consistent allocations such descriptors. */structlist_headdma_pools;/* dma pools (if dma'ble) */structdma_coherent_mem*dma_mem;/* internal for coherent mem override *//* arch specific additions */structdev_archdataarchdata;spinlock_tdevres_lock;structlist_headdevres_head;/* class_device migration path */structlist_headnode;structclass*class;dev_tdevt;/* dev_t, creates the sysfs "dev" */structattribute_group**groups;/* optional groups */void(*release)(structdevice*dev);/*当这个设备的最后引用被删除时,内核调用该方法; 它从被嵌入的 kobject 的 release 方法中调用。所有注册到核心的设备结构必须有一个 release 方法, 否则内核将打印错误信息*/};/*在注册 struct device 前,最少要设置parent, bus_id, bus, 和 release 成员*/

设备注册

设备的注册和注销函数为:

intdevice_register(structdevice*dev);voiddevice_unregister(structdevice*dev);

一个实际的总线也是一个设备,所以必须单独注册,以下为 lddbus 在编译时注册它的虚拟总线设备源码:

staticvoidldd_bus_release(structdevice*dev){printk(KERN_DEBUG"lddbus release\n");}structdevice ldd_bus={.bus_id="ldd0",.release=ldd_bus_release};/*这是顶层总线,parent 和 bus 成员为 NULL*//*作为第一个(并且唯一)总线, 它的名字为 ldd0,这个总线设备的注册代码如下:*/ret=device_register(&ldd_bus);if(ret)printk(KERN_NOTICE"Unable to register ldd0\n");/*一旦调用完成, 新总线会在 sysfs 中 /sys/devices 下显示,任何挂到这个总线的设备会在 /sys/devices/ldd0 下显示*/

设备属性

sysfs 中的设备入口可有属性,相关的结构是:

/* interface for exporting device attributes这个结构体和《LDD3》中的不同,已经被更新过了,请特别注意!*/structdevice_attribute{structattribute attr;ssize_t(*show)(structdevice*dev,struct device_attribute *attr,char*buf);ssize_t(*store)(structdevice*dev,struct device_attribute *attr,constchar*buf,size_t count);};/*设备属性结构可在编译时建立, 使用以下宏:*/DEVICE_ATTR(_name,_mode,_show,_store);/*这个宏声明一个结构, 将 dev_attr_ 作为给定 _name 的前缀来命名设备属性/*属性文件的实际处理使用以下函数:*/intdevice_create_file(structdevice*device,structdevice_attribute*entry);voiddevice_remove_file(structdevice*dev,structdevice_attribute*attr);

设备结构的嵌入

device 结构包含设备模型核心用来模拟系统的信息。但大部分子系统记录了关于它们又拥有的设备的额外信息,所以很少单纯用 device 结构代表设备,而是,通常将其嵌入一个设备的高层表示中。底层驱动几乎不知道 struct device。

lddbus 驱动创建了它自己的 device 类型,并期望每个设备驱动使用这个类型来注册它们的设备:

structldd_device{char*name;structldd_driver*driver;structdevice dev;};#defineto_ldd_device(dev)container_of(dev,structldd_device,dev);

lddbus 导出的注册和注销接口如下:

/** LDD devices.*//** For now, no references to LDDbus devices go out which are not* tracked via the module reference count, so we use a no-op* release function.*/staticvoidldd_dev_release(structdevice*dev){}intregister_ldd_device(structldd_device*ldddev){ldddev->dev.bus=&ldd_bus_type;ldddev->dev.parent=&ldd_bus;ldddev->dev.release=ldd_dev_release;strncpy(ldddev->dev.bus_id,ldddev->name,BUS_ID_SIZE);returndevice_register(&ldddev->dev);}EXPORT_SYMBOL(register_ldd_device);voidunregister_ldd_device(structldd_device*ldddev){device_unregister(&ldddev->dev);}EXPORT_SYMBOL(unregister_ldd_device);

sculld 驱动添加一个自己的属性到它的设备入口,称为 dev, 仅包含关联的设备号,源码如下:

staticssize_t sculld_show_dev(structdevice*ddev,struct device_attribute *attr,char*buf){structsculld_dev*dev=ddev->driver_data;returnprint_dev_t(buf,dev->cdev.dev);}staticDEVICE_ATTR(dev,S_IRUGO,sculld_show_dev,NULL);/*接着, 在初始化时间, 设备被注册, 并且 dev 属性通过下面的函数被创建:*/staticvoidsculld_register_dev(structsculld_dev*dev,intindex){sprintf(dev->devname,"sculld%d",index);dev->ldev.name=dev->devname;dev->ldev.driver=&sculld_driver;dev->ldev.dev.driver_data=dev;register_ldd_device(&dev->ldev);if (device_create_file(&dev->ldev.dev, &dev_attr_dev)) printk( "Unable to create dev attribute ! \n");}/*注意:程序使用 driver_data 成员来存储指向我们自己的内部的设备结构的指针。请检查device_create_file的返回值,否则编译时会有警告。*/


设备驱动程序

设备模型跟踪所有系统已知的驱动,主要目的是使驱动程序核心能协调驱动和新设备之间的关系。一旦驱动在系统中是已知的对象就可能完成大量的工作。驱动程序的结构体 device_driver 定义如下:

/*定义在<linux/device.h>*/structdevice_driver{constchar*name;/*驱动程序的名字( 在 sysfs 中出现 )*/structbus_type*bus;/*驱动程序所操作的总线类型*/structkobjectkobj;/*内嵌的kobject对象*/structklistklist_devices;/*当前驱动程序能操作的设备链表*/structklist_nodeknode_bus;structmodule*owner;constchar*mod_name;/* used for built-in modules */structmodule_kobject*mkobj;int(*probe)(structdevice*dev);/*查询一个特定设备是否存在及驱动是否可以使用它的函数*/int(*remove)(structdevice*dev);/*将设备从系统中删除*/void(*shutdown)(structdevice*dev);/*关闭设备*/int(*suspend)(structdevice*dev,pm_message_t state);int(*resume)(structdevice*dev);};/*注册device_driver 结构的函数是:*/intdriver_register(structdevice_driver*drv);voiddriver_unregister(structdevice_driver*drv);/*driver的属性结构在:*/structdriver_attribute{structattribute attr;ssize_t(*show)(structdevice_driver*drv,char*buf);ssize_t(*store)(structdevice_driver*drv,constchar*buf,size_tcount);};DRIVER_ATTR(_name,_mode,_show,_store)/*属性文件创建的方法:*/intdriver_create_file(structdevice_driver*drv,structdriver_attribute*attr);voiddriver_remove_file(structdevice_driver*drv,structdriver_attribute*attr);/*bus_type 结构含有一个成员( drv_attrs ) 指向一组为属于该总线的所有设备创建的默认属性*/

在更新的内核里,这个结构体变得更简洁了,隐藏了无需驱动编程人员知道的一些成员:

/*in Linux 2.6.26.5*/

structdevice_driver{constchar*name;structbus_type*bus;structmodule*owner;constchar*mod_name;/* used for built-in modules */int(*probe)(structdevice*dev);int(*remove)(structdevice*dev);void(*shutdown)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_t state);int(*resume)(structdevice*dev);structattribute_group**groups;structdriver_private*p;};structdriver_private{structkobject kobj;structklist klist_devices;structklist_node knode_bus;structmodule_kobject*mkobj;structdevice_driver*driver;};#defineto_driver(obj)container_of(obj,structdriver_private,kobj)

驱动程序结构的嵌入

对大多数驱动程序核心结构, device_driver 结构通常被嵌入到一个更高层的、总线相关的结构中。

以lddbus 子系统为例,它定义了ldd_driver 结构:

structldd_driver{char*version;structmodule*module;structdevice_driver driver;structdriver_attribute version_attr;};#defineto_ldd_driver(drv)container_of(drv,structldd_driver,driver);

lddbus总线中相关的驱动注册和注销函数是:

/** Crude driver interface.*/staticssize_t show_version(structdevice_driver*driver,char*buf){structldd_driver*ldriver=to_ldd_driver(driver);sprintf(buf,"%s\n",ldriver->version);returnstrlen(buf);}intregister_ldd_driver(structldd_driver*driver){intret;driver->driver.bus=&ldd_bus_type;ret=driver_register(&driver->driver);/*注册底层的 device_driver 结构到核心*/if(ret)returnret;driver->version_attr.attr.name="version";/* driver_attribute 结构必须手工填充*/driver->version_attr.attr.owner=driver->module;/*注意:设定 version 属性的拥有者为驱动模块, 不是 lddbus 模块!因为 show_version 函数是使用驱动模块所创建的 ldd_driver 结构,若 ldd_driver 结构在一个用户空间进程试图读取版本号时已经注销,就会出错*/driver->version_attr.attr.mode=S_IRUGO;driver->version_attr.show=show_version;driver->version_attr.store=NULL;returndriver_create_file(&driver->driver,&driver->version_attr);/*建立版本属性,因为这个属性在运行时被创建,所以不能使用 DRIVER_ATTR 宏*/}voidunregister_ldd_driver(structldd_driver*driver){driver_unregister(&driver->driver);}EXPORT_SYMBOL(register_ldd_driver);EXPORT_SYMBOL(unregister_ldd_driver);

在sculld 中创建的 ldd_driver 结构如下:

/* Device model stuff */staticstructldd_driver sculld_driver={.version="$Revision: 1.21 $",.module=THIS_MODULE,.driver={.name="sculld",},};/*只要一个简单的 register_ldd_driver 调用就可添加它到系统中。一旦完成初始化, 驱动信息可在 sysfs 中显示*/


类 子系统

类是一个设备的高层视图, 它抽象出了底层的实现细节,从而允许用户空间使用设备所提供的功能, 而不用关心设备是如何连接和工作的。类成员通常由上层代码所控制, 而无需驱动的明确支持。但有些情况下驱动也需要直接处理类。

几乎所有的类都显示在 /sys/class 目录中。出于历史的原因,有一个例外:块设备显示在 /sys/block目录中。在许多情况, 类子系统是向用户空间导出信息的最好方法。当类子系统创建一个类时, 它将完全拥有这个类,根本不用担心哪个模块拥有那些属性,而且信息的表示也比较友好。

为了管理类,驱动程序核心导出了一些接口,其目的之一是提供包含设备号的属性以便自动创建设备节点,所以udev的使用离不开类。类函数和结构与设备模型的其他部分遵循相同的模式,所以真正崭新的概念是很少的。

注意:class_simple 是老接口,在2.6.13中已被删除,这里不再研究。

管理类的接口

类由 struct class 的结构体来定义:

/** device classes*/structclass{constchar*name;/*每个类需要一个唯一的名字, 它将显示在 /sys/class 中*/structmodule*owner;structksetsubsys;structlist_headchildren;structlist_headdevices;structlist_headinterfaces;structksetclass_dirs;structsemaphoresem;/* locks both the children and interfaces lists */structclass_attribute*class_attrs;/* 指向类属性的指针(以NULL结尾) */structclass_device_attribute*class_dev_attrs;/* 指向类中每个设备的一组默认属性的指针 */structdevice_attribute*dev_attrs;int(*uevent)(structclass_device*dev,char**envp,intnum_envp,char*buffer,intbuffer_size);/* 类热插拔产生时添加环境变量的函数 */int(*dev_uevent)(structdevice*dev,char**envp,intnum_envp,char*buffer,intbuffer_size);/* 类中的设备热插拔时添加环境变量的函数 */void(*release)(structclass_device*dev);/* 把设备从类中删除的函数 */void(*class_release)(structclass*class);/* 删除类本身的函数 */void(*dev_release)(structdevice*dev);int(*suspend)(structdevice*,pm_message_t state);int(*resume)(structdevice*);};/*类注册函数:*/intclass_register(structclass*cls);voidclass_unregister(structclass*cls);/*类属性的接口:*/structclass_attribute{structattribute attr;ssize_t(*show)(structclass*cls,char*buf);ssize_t(*store)(structclass*cls,constchar*buf,size_tcount);};CLASS_ATTR(_name,_mode,_show,_store);intclass_create_file(structclass*cls,conststructclass_attribute*attr);voidclass_remove_file(structclass*cls,conststructclass_attribute*attr);

在更新的内核里,这个结构体变得简洁了,删除了一些成员:

/*in Linux 2.6.26.5*//** device classes*/structclass{constchar*name;structmodule*owner;structksetsubsys;structlist_headdevices;structlist_headinterfaces;structksetclass_dirs;structsemaphoresem;/* locks children, devices, interfaces */structclass_attribute*class_attrs;structdevice_attribute*dev_attrs;int(*dev_uevent)(structdevice*dev,structkobj_uevent_env*env);void(*class_release)(structclass*class);void(*dev_release)(structdevice*dev);int(*suspend)(structdevice*dev,pm_message_t state);int(*resume)(structdevice*dev);};

类设备(在新内核中已被删除)

类存在的真正目的是给作为类成员的各个设备提供一个容器,成员由 struct class_device 来表示:

structclass_device{structlist_headnode;/*for internal use by the driver core only*/structkobjectkobj;/*for internal use by the driver core only*/structclass*class;/* 指向该设备所属的类,必须*/dev_tdevt;/* dev_t, creates the sysfs "dev" ,for internal use by the driver core only*/structclass_device_attribute*devt_attr;/*for internal use by the driver core only*/structclass_device_attribute uevent_attr;structdevice*dev;/* 指向此设备相关的 device 结构体,可选。若不为NULL,应是一个从类入口到/sys/devices 下相应入口的符号连接,以便用户空间查找设备入口*/void*class_data;/* 私有数据指针 */structclass_device*parent;/* parent of this child device, if there is one */structattribute_group**groups;/* optional groups */void(*release)(structclass_device*dev);int(*uevent)(structclass_device*dev,char**envp,intnum_envp,char*buffer,intbuffer_size);charclass_id[BUS_ID_SIZE];/* 此类中的唯一的名字 */};/*类设备注册函数:*/intclass_device_register(structclass_device*cd);voidclass_device_unregister(structclass_device*cd);/*重命名一个已经注册的类设备入口:*/intclass_device_rename(structclass_device*cd,char*new_name);/*类设备入口属性:*/structclass_device_attribute{structattribute attr;ssize_t(*show)(structclass_device*cls,char*buf);ssize_t(*store)(structclass_device*cls,constchar*buf,size_tcount);};CLASS_DEVICE_ATTR(_name,_mode,_show,_store);/*创建和删除除struct class中设备默认属性外的属性*/intclass_device_create_file(structclass_device*cls,conststructclass_device_attribute*attr);voidclass_device_remove_file(structclass_device*cls,conststructclass_device_attribute*attr);

类接口

类子系统有一个 Linux 设备模型的其他部分找不到的附加概念,称为“接口”, 可将它理解为一种设备加入或离开类时获得信息的触发机制,结构体如下:

structclass_interface{structlist_headnode;structclass*class;/* 指向该接口所属的类*/int(*add)(structclass_device*,structclass_interface*);

/*当一个类设备被加入到在 class_interface 结构中指定的类时, 将调用接口的 add 函数,进行一些设备需要的额外设置,通常是添加更多属性或其他的一些工作*/void(*remove)(structclass_device*,structclass_interface*);/*一个接口的功能是简单明了的. 当设备从类中删除, 将调用remove 方法来进行必要的清理*/int(*add_dev)(structdevice*,structclass_interface*);void(*remove_dev)(structdevice*,structclass_interface*);};/*注册或注销接口的函数:*/intclass_interface_register(structclass_interface*class_intf);voidclass_interface_unregister(structclass_interface*class_intf);/*一个类可注册多个接口*/

参考:http://www.cnblogs.com/andtt/articles/2178905.html

在乎的是看风景的心情,旅行不会因为美丽的风景终止。

Linux设备模型 — 总线、设备、驱动程序和类(机制理解)

相关文章:

你感兴趣的文章:

标签云: