前提概要
众所周知,计算机的核心是CPU,Linux系统就是运行在CPU之上的。但是仅仅有CPU是不够的,还需要有内存、磁盘、键盘、鼠标、显示器等各种各样的设备。Linux系统运行起来就需要将各种设备接入到计算机当中,那么他们是怎么和CPU联系在一起的呢?
通过上图我们可以看到,CPU出来后FBS总线连接北桥,北桥有PCIe和Memory总线连接显卡和内存,这些都是高速数据传输设备,同时北桥还有internal总线连接南桥;南桥是作为连接慢速数据传输设备而存在的,它则通过PCI和LPC总线与各式设备相连接。
由此可见,CPU去操作设备,需要经过总线才能到达设备,而且到达设备会经过不同的总线。
总线是什么?维基的定义:它是指计算机组件间规范化的交换数据的方式,即以一种通用的方式为各组件提供数据传送和控制逻辑。通俗点说就是它有着特定的排线数量(总线宽度)、特定的数据速率(电平宽度)以及特定的数据格式(总线命令)的一个数据线,然后可以在这数据线上整成各式的接口,例如USB总线就可以有Type-A、Type-C,还可以有Mini-A和Minit-B等等。
设备是什么?键盘、鼠标、显示器、打印机都是设备,这是常用的外接设备,而对于计算机而言,磁盘、显卡、声卡、光驱也都是设备,甚至内存也是设备,较为特殊的设备。总而言之,真实具体的物理器件都是设备,在软件层面,它有着自己独特的参数和属性。作为一个设备接入到计算机当中,它要有一个硬件接口接入到总线中,这个接口的线宽必然需要和总线宽度一致,而接入到总线后,要能响应总线上的命令才能够被正常使用,这就要求它要有和总线相同的速率才能识别,同时还要有和总线传输一致的数据格式。
驱动是什么?设备是多式多样的,功能更是纷繁复杂,不同总线的设备传输的数据时序和命令是不同的;即便是同总线上的不同设备虽然被总线设定好了要求的时序和命令,但是设备响应命令以及数据反馈的方式也是不同的,有的是读datasheet规定的地址上的数据,有的是读取存储的内容;甚至是同总线的同类设备,数据反馈也不一样,例如触摸屏也区分单点和多点,两者反馈是不一样的。所以驱动就是为了应对各式各样的设备,为操作设备提供操作接口。
设备驱动管理
- 面向对象的管理
面向对象的思想就是一切事物皆对象,Linux的设备驱动管理将运用这一思想对各式各样的设备、总线以及驱动进行管理。在此可以感受到老子说的:一生二,二生三,三生万物。
1)对象的“一生二”
面向对象,首先要定义了一个基础的对象类型kobject。
struct kobject { const char *name; struct list_head entry; struct kobject *parent; struct kset *kset; struct kobj_type *ktype; struct kernfs_node *sd; /* sysfs directory entry */ struct kref kref; #ifdef CONFIG_DEBUG_KOBJECT_RELEASE struct delayed_work release; #endif unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; };
这里面有对象类型名字、管理链表等数据信息。其中parent表示父对象,诸如总线和设备就是父子关系。而sd则与sysfs的目录项进行关联,简而言之就是每一个kobject都对应着一个sysfs下面的目录。至于kref主要是对该对象的引用进行计数管理。
对其中 ktype 的类型kobj_type进行展开:
struct kobj_type { void (*release)(struct kobject *kobj); const struct sysfs_ops *sysfs_ops; struct attribute **default_attrs; /* use default_groups instead */ const struct attribute_group **default_groups; const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj); const void *(*namespace)(struct kobject *kobj); void (*get_ownership)(struct kobject *kobj, kuid_t *uid, kgid_t *gid); };
这里面很清晰地看到它定义了一些操作接口,包括sysfs_ops这是打开sysfs文件对象后的操作接口集合。重点关注default_attrs和default_groups,他们都是表示对象的属性信息。
展开default_groups的attribute_group,可以看到:
struct attribute_group { const char *name; umode_t (*is_visible)(struct kobject *, struct attribute *, int); umode_t (*is_bin_visible)(struct kobject *, struct bin_attribute *, int); struct attribute **attrs; struct bin_attribute **bin_attrs; };
这里作为属性集合,里面涵盖的attrs和bin_attrs同样是表示属性集合,而且有更多的操作接口,其中bin_attrs表示模块二进制的操作接口的,它通常用在/sys/module下面,表示内核模块的属性管理,如果展开的话,它里面同样含有struct attribute结构,在设备驱动管理中基本不涉及,这里不再细诉。由此可见default_groups比default_attrs更全,预期未来将会替代default_attrs而存在。
接下来看一下kset结构:
struct kset { struct list_head list; spinlock_t list_lock; struct kobject kobj; const struct kset_uevent_ops *uevent_ops; } __randomize_layout;
Kset表示的是对对象的一个管理集合,可以看到它拥有管理数据成员list和list_lock,这是对kobject的管理,以及uevent_ops这是对事件的响应处理函数集。同时根据kobj类型可以看到它也是一个kobject对象。正如部队里面的排长和战士的关系,排长管着一个排的兵,但是他自己同时也是一个兵。
2)对象的“二生三”
管理的基础对象kobject和kset定义好了,那么就需要开始贴合实际,延伸出一些通用的管理类型结构。于是就有了:设备、驱动和总线,它们都将视为对象而存在。
首先看一下设备的类型是device,分析一下其定义:
struct device { struct kobject kobj; struct device *parent; struct device_private *p; const char *init_name; /* initial name of the device */ const struct device_type *type; struct bus_type *bus; /* type of bus device is on */ struct device_driver *driver; /* which driver has allocated this device */ void *platform_data; /* Platform specific data, device core doesn't touch it */ void *driver_data; /* Driver data, set and get with dev_set_drvdata/dev_get_drvdata */ #ifdef CONFIG_PROVE_LOCKING struct mutex lockdep_mutex; #endif struct mutex mutex; /* mutex to synchronize calls to * its driver. */ struct dev_links_info links; struct dev_pm_info power; struct dev_pm_domain *pm_domain; #ifdef CONFIG_GENERIC_MSI_IRQ_DOMAIN struct irq_domain *msi_domain; #endif #ifdef CONFIG_PINCTRL struct dev_pin_info *pins; #endif #ifdef CONFIG_GENERIC_MSI_IRQ struct list_head msi_list; #endif const struct dma_map_ops *dma_ops; u64 *dma_mask; /* dma mask (if dma'able device) */ u64 coherent_dma_mask;/* Like dma_mask, but for alloc_coherent mappings as not all hardware supports 64 bit addresses for consistent allocations such descriptors. */ u64 bus_dma_mask; /* upstream dma_mask constraint */ unsigned long dma_pfn_offset; struct device_dma_parameters *dma_parms; struct list_head dma_pools; /* dma pools (if dma'ble) */ #ifdef CONFIG_DMA_DECLARE_COHERENT struct dma_coherent_mem *dma_mem; /* internal for coherent mem override */ #endif #ifdef CONFIG_DMA_CMA struct cma *cma_area; /* contiguous memory area for dma allocations */ #endif /* arch specific additions */ struct dev_archdata archdata; struct device_node *of_node; /* associated device tree node */ struct fwnode_handle *fwnode; /* firmware device node */ #ifdef CONFIG_NUMA int numa_node; /* NUMA node this device is close to */ #endif dev_t devt; /* dev_t, creates the sysfs "dev" */ u32 id; /* device instance */ spinlock_t devres_lock; struct list_head devres_head; struct class *class; const struct attribute_group **groups; /* optional groups */ void (*release)(struct device *dev); struct iommu_group *iommu_group; struct iommu_fwspec *iommu_fwspec; struct iommu_param *iommu_param; bool offline_disabled:1; bool offline:1; bool of_node_reused:1; #if defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_DEVICE) || \ defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU) || \ defined(CONFIG_ARCH_HAS_SYNC_DMA_FOR_CPU_ALL) bool dma_coherent:1; #endif };
Device的结构体定义中先是看到了kobj的存在,意味着它就是一个对象,然后可以看到它还有私有数据device_private,同时还表明它处于什么总线(bus_type)下面以及由什么驱动(device_driver)去操作,再者就是它的属性信息、操作接口和管理信息等内容。
再接下来看一下驱动的数据结构device_driver,它的定义:
struct device_driver { const char *name; struct bus_type *bus; struct module *owner; const char *mod_name; /* used for built-in modules */ bool suppress_bind_attrs; /* disables bind/unbind via sysfs */ enum probe_type probe_type; const struct of_device_id *of_match_table; const struct acpi_device_id *acpi_match_table; int (*probe) (struct device *dev); int (*remove) (struct device *dev); void (*shutdown) (struct device *dev); int (*suspend) (struct device *dev, pm_message_t state); int (*resume) (struct device *dev); const struct attribute_group **groups; const struct attribute_group **dev_groups; const struct dev_pm_ops *pm; void (*coredump) (struct device *dev); struct driver_private *p; };
device_driver的结构体定义设备name名字、归属的bus总线以及支持的设备ID信息,同时还有各种操作接口,主要的有probe探测设备专用的接口和remove、shutdown等等设备热插拔会用到的接口,最后肯定还少不了驱动属性attribute_group的数据信息和dev_pm_ops电源管理操作接口。说好的驱动也是对象呢?kobject在哪?它的kobject就在末尾的driver_private数据结构中。driver_private结构很精练:
struct driver_private { struct kobject kobj; struct klist klist_devices; struct klist_node knode_bus; struct module_kobject *mkobj; struct device_driver *driver; };
它就定义了kobject,表示驱动也是一个对象,然后除此之外还有设备链表、模块信息以及关联回去驱动的指针等。
最后,总线的数据类型是bus_type,分析一下其定义:
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)(struct device *dev, struct device_driver *drv); int (*uevent)(struct device *dev, struct kobj_uevent_env *env); int (*probe)(struct device *dev); int (*remove)(struct device *dev); void (*shutdown)(struct device *dev); int (*online)(struct device *dev); int (*offline)(struct device *dev); int (*suspend)(struct device *dev, pm_message_t state); int (*resume)(struct device *dev); int (*num_vf)(struct device *dev); int (*dma_configure)(struct device *dev); const struct dev_pm_ops *pm; const struct iommu_ops *iommu_ops; struct subsys_private *p; struct lock_class_key lock_key; bool need_parent_lock; };
总线也是有名字的,定义了总线名字name,以及各种属性集合bus_groups、dev_groups和drv_groups。接着还有总线的各种操作接口,比较值得关注的是match和probe,分别用于判断设备是否与总线匹配以及对设备进行探测。多说一下,probe虽然直译是探测,实际上干得可不止那一点半点的事情,它包括了设备初始化和数据定义等一堆事情。同时应该也注意到driver也有个probe。对,他们的职能是一样的,但是如果要是总线定义有probe,那么在match设备之后,如果bus的probe是定义的情况下,将会使用bus的probe去初始化设备,除非bus的probe为null没有定义的情况下,才会使用driver的probe。为什么这么做呢?因为设备初始化的时候,往往需要有内存地址映射,将设备的缓存或者寄存器投射到内存地址空间中,如此才能通过对内存的读写去访问设备,而总线,例如PCI总线,它的设备众多,各映射各的会乱套,需要有一个掌管者去分配,它就是bus。说得多一些,即便不定义总线的probe,靠驱动的probe去做,也没什么,这就增大了驱动开发者的工作量了,同时为了避免地址冲突,也必然会增加多一个关于地址映射分配的公共模块,这会导致架构腐化了。除此之外,免不了的还有设备电源管理dev_pm_ops接口集以及内存地址映射的iommu_ops接口。
末了还是会有隐藏起来的kobject。它就在subsys_private结构里面。
struct subsys_private { struct kset subsys; struct kset *devices_kset; struct list_head interfaces; struct mutex mutex; struct kset *drivers_kset; struct klist klist_devices; struct klist klist_drivers; struct blocking_notifier_head bus_notifier; unsigned int drivers_autoprobe:1; struct bus_type *bus; struct kset glue_dirs; struct class *class; };
这结构体定义了很多管理结构,而它作为对象的kobject则是在subsys这个kset结构体中。
到这里就“二生三”,衍生出了设备、驱动和总线。但是事情还没完,有些设备或者总线,它们是具备一些共同的属性或者操作接口,于是乎就新增了一个从高层次视角去抽象出来的对象,它就是class数据结构,回顾前面的总线和设备的定义,都可以看到有这么一个class。
struct class { const char *name; struct module *owner; const struct attribute_group **class_groups; const struct attribute_group **dev_groups; struct kobject *dev_kobj; int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env); char *(*devnode)(struct device *dev, umode_t *mode); void (*class_release)(struct class *class); void (*dev_release)(struct device *dev); int (*shutdown_pre)(struct device *dev); const struct kobj_ns_type_operations *ns_type; const void *(*namespace)(struct device *dev); void (*get_ownership)(struct device *dev, kuid_t *uid, kgid_t *gid); const struct dev_pm_ops *pm; struct subsys_private *p; };
同样,class具有自己的名字name、归属模块owner以及属性集合class_groups和dev_groups,还有各种接口以及电源管理操作集。同样,它的kobject是放置在了subsys_private里面,总线也是有这么一个subsys_private的。class其实就类似于面向对象的类,拥有共同的数据和属性的对象就可以共同继承这个类,虽说bus也有这么一个class,目前代码中并没有看到一个很明确的bus共同属性的class。
3)对象的“三生万物”
到现在已经定义好了设备、驱动、总线和类。如何衍生系统中的设备万物呢?
设备种类纷繁复杂,我们必须挑个典型来看,否则就有可能迷失其中,那就从PCI开始,以PCI进行展开,然后举一反三进行类比思考。
首先还是设备相关的,PCI总线上的设备都会有一个设备类型,它就是pci_dev,它的结构体比较庞大。
struct pci_dev { struct list_head bus_list; /* Node in per-bus list */ struct pci_bus *bus; /* Bus this device is on */ struct pci_bus *subordinate; /* Bus this device bridges to */ void *sysdata; /* Hook for sys-specific extension */ struct proc_dir_entry *procent; /* Device entry in /proc/bus/pci */ struct pci_slot *slot; /* Physical slot this device is in */ unsigned int devfn; /* Encoded device & function index */ unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ u8 revision; /* PCI revision, low byte of class word */ u8 hdr_type; /* PCI header type (`multi' flag masked out) */ #ifdef CONFIG_PCIEAER u16 aer_cap; /* AER capability offset */ struct aer_stats *aer_stats; /* AER stats for this device */ #endif u8 pcie_cap; /* PCIe capability offset */ u8 msi_cap; /* MSI capability offset */ u8 msix_cap; /* MSI-X capability offset */ u8 pcie_mpss:3; /* PCIe Max Payload Size Supported */ u8 rom_base_reg; /* Config register controlling ROM */ u8 pin; /* Interrupt pin this device uses */ u16 pcie_flags_reg; /* Cached PCIe Capabilities Register */ unsigned long *dma_alias_mask;/* Mask of enabled devfn aliases */ struct pci_driver *driver; /* Driver bound to this device */ u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */ struct device_dma_parameters dma_parms; pci_power_t current_state; /* Current operating state. In ACPI, this is D0-D3, D0 being fully functional, and D3 being off. */ unsigned int imm_ready:1; /* Supports Immediate Readiness */ u8 pm_cap; /* PM capability offset */ unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */ unsigned int pme_poll:1; /* Poll device's PME status bit */ unsigned int d1_support:1; /* Low power state D1 is supported */ unsigned int d2_support:1; /* Low power state D2 is supported */ unsigned int no_d1d2:1; /* D1 and D2 are forbidden */ unsigned int no_d3cold:1; /* D3cold is forbidden */ unsigned int bridge_d3:1; /* Allow D3 for bridge */ unsigned int d3cold_allowed:1; /* D3cold is allowed by user */ unsigned int mmio_always_on:1; /* Disallow turning off io/mem decoding during BAR sizing */ unsigned int wakeup_prepared:1; unsigned int runtime_d3cold:1; /* Whether go through runtime D3cold, not set for devices powered on/off by the corresponding bridge */ unsigned int skip_bus_pm:1; /* Internal: Skip bus-level PM */ unsigned int ignore_hotplug:1; /* Ignore hotplug events */ unsigned int hotplug_user_indicators:1; /* SlotCtl indicators controlled exclusively by user sysfs */ unsigned int clear_retrain_link:1; /* Need to clear Retrain Link bit manually */ unsigned int d3_delay; /* D3->D0 transition time in ms */ unsigned int d3cold_delay; /* D3cold->D0 transition time in ms */ #ifdef CONFIG_PCIEASPM struct pcie_link_state *link_state; /* ASPM link state */ unsigned int ltr_path:1; /* Latency Tolerance Reporting supported from root to here */ #endif unsigned int eetlp_prefix_path:1; /* End-to-End TLP Prefix */ pci_channel_state_t error_state; /* Current connectivity state */ struct device dev; /* Generic device interface */ int cfg_size; /* Size of config space */ /* * Instead of touching interrupt line and base address registers * directly, use the values stored here. They might be different! */ unsigned int irq; struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */ bool match_driver; /* Skip attaching driver */ unsigned int transparent:1; /* Subtractive decode bridge */ unsigned int io_window:1; /* Bridge has I/O window */ unsigned int pref_window:1; /* Bridge has pref mem window */ unsigned int pref_64_window:1; /* Pref mem window is 64-bit */ unsigned int multifunction:1; /* Multi-function device */ unsigned int is_busmaster:1; /* Is busmaster */ unsigned int no_msi:1; /* May not use MSI */ unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */ unsigned int block_cfg_access:1; /* Config space access blocked */ unsigned int broken_parity_status:1; /* Generates false positive parity */ unsigned int irq_reroute_variant:2; /* Needs IRQ rerouting variant */ unsigned int msi_enabled:1; unsigned int msix_enabled:1; unsigned int ari_enabled:1; /* ARI forwarding */ unsigned int ats_enabled:1; /* Address Translation Svc */ unsigned int pasid_enabled:1; /* Process Address Space ID */ unsigned int pri_enabled:1; /* Page Request Interface */ unsigned int is_managed:1; unsigned int needs_freset:1; /* Requires fundamental reset */ unsigned int state_saved:1; unsigned int is_physfn:1; unsigned int is_virtfn:1; unsigned int reset_fn:1; unsigned int is_hotplug_bridge:1; unsigned int shpc_managed:1; /* SHPC owned by shpchp */ unsigned int is_thunderbolt:1; /* Thunderbolt controller */ /* * Devices marked being untrusted are the ones that can potentially * execute DMA attacks and similar. They are typically connected * through external ports such as Thunderbolt but not limited to * that. When an IOMMU is enabled they should be getting full * mappings to make sure they cannot access arbitrary memory. */ unsigned int untrusted:1; unsigned int __aer_firmware_first_valid:1; unsigned int __aer_firmware_first:1; unsigned int broken_intx_masking:1; /* INTx masking can't be used */ unsigned int io_window_1k:1; /* Intel bridge 1K I/O windows */ unsigned int irq_managed:1; unsigned int non_compliant_bars:1; /* Broken BARs; ignore them */ unsigned int is_probed:1; /* Device probing in progress */ unsigned int link_active_reporting:1;/* Device capable of reporting link active */ unsigned int no_vf_scan:1; /* Don't scan for VFs after IOV enablement */ pci_dev_flags_t dev_flags; atomic_t enable_cnt; /* pci_enable_device has been called */ u32 saved_config_space[16]; /* Config space saved at suspend time */ struct hlist_head saved_cap_space; struct bin_attribute *rom_attr; /* Attribute descriptor for sysfs ROM entry */ int rom_attr_enabled; /* Display of ROM attribute enabled? */ struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */ struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */ #ifdef CONFIG_HOTPLUG_PCI_PCIE unsigned int broken_cmd_compl:1; /* No compl for some cmds */ #endif #ifdef CONFIG_PCIE_PTM unsigned int ptm_root:1; unsigned int ptm_enabled:1; u8 ptm_granularity; #endif #ifdef CONFIG_PCI_MSI const struct attribute_group **msi_irq_groups; #endif struct pci_vpd *vpd; #ifdef CONFIG_PCI_ATS union { struct pci_sriov *sriov; /* PF: SR-IOV info */ struct pci_dev *physfn; /* VF: related PF */ }; u16 ats_cap; /* ATS Capability offset */ u8 ats_stu; /* ATS Smallest Translation Unit */ atomic_t ats_ref_cnt; /* Number of VFs with ATS enabled */ #endif #ifdef CONFIG_PCI_PRI u32 pri_reqs_alloc; /* Number of PRI requests allocated */ #endif #ifdef CONFIG_PCI_PASID u16 pasid_features; #endif #ifdef CONFIG_PCI_P2PDMA struct pci_p2pdma *p2pdma; #endif phys_addr_t rom; /* Physical address if not from BAR */ size_t romlen; /* Length if not from BAR */ char *driver_override; /* Driver name to force a match */ unsigned long priv_flags; /* Private flags for the PCI driver */ };
结构虽大,而且数据信息量也不小,不过这些信息基本都是围绕着PCI设备的属性进行描述的,与驱动框架无关。既然说是面向对象的管理框架,那么我们可以看到有这么一个数据成员:struct device dev。对的,它就是继承了device特性扩展而来的,在Linux的设备驱动管理框架中则可以看到这么一个宏定义to_pci_dev,通过device找到pci_dev结构。同样的,类似的usb_device,定义usb设备类型的,它里面同样也嵌入了一个device结构成员,它也有一个to_usb_device的宏定义。继续推进i2c_device也是嵌入了device结构成员,只是它通过了i2c_adapter间接嵌入的,但是它同样有一个偏移定位结构位置的宏定义to_i2c_adapter,虽然只是定位到i2c_adapter而已。
接下来我们看一下关于PCI设备的驱动是不是也类似这样的呢?
PCI设备的驱动确实也有一个结构体定义,那就是pci_driver,不过它就比较精炼了。
struct pci_driver { struct list_head node; const char *name; const struct pci_device_id *id_table; /* Must be non-NULL for probe to be called */ int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ void (*remove)(struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ int (*suspend)(struct pci_dev *dev, pm_message_t state); /* Device suspended */ int (*suspend_late)(struct pci_dev *dev, pm_message_t state); int (*resume_early)(struct pci_dev *dev); int (*resume)(struct pci_dev *dev); /* Device woken up */ void (*shutdown)(struct pci_dev *dev); int (*sriov_configure)(struct pci_dev *dev, int num_vfs); /* On PF */ const struct pci_error_handlers *err_handler; const struct attribute_group **groups; struct device_driver driver; struct pci_dynids dynids; };
它定义了设备名、PCI设备管理数据以及操作接口,同时还有PCI特有的属性集合attribute_group。于是就可以看到我们关心的struct device_driver driver。是的,它也是从device_driver衍生出来的数据类型,同样,它也类似于设备一样,有一个偏移定位结构位置的宏定义to_pci_driver。类似的USB设备驱动有usb_driver,I2C设备驱动有i2c_driver。
至于bus_type是否也有衍生结构呢?它是没有的。那么前面pci_dev里面的pci_bus类型是什么?我们可以看一下它的定义:
struct pci_bus { struct list_head node; /* Node in list of buses */ struct pci_bus *parent; /* Parent bus this bridge is on */ struct list_head children; /* List of child buses */ struct list_head devices; /* List of devices on this bus */ struct pci_dev *self; /* Bridge device as seen by parent */ struct list_head slots; /* List of slots on this bus; protected by pci_slot_mutex */ struct resource *resource[PCI_BRIDGE_RESOURCE_NUM]; struct list_head resources; /* Address space routed to this bus */ struct resource busn_res; /* Bus numbers routed to this bus */ struct pci_ops *ops; /* Configuration access functions */ struct msi_controller *msi; /* MSI controller */ void *sysdata; /* Hook for sys-specific extension */ struct proc_dir_entry *procdir; /* Directory entry in /proc/bus/pci */ unsigned char number; /* Bus number */ unsigned char primary; /* Number of primary bridge */ unsigned char max_bus_speed; /* enum pci_bus_speed */ unsigned char cur_bus_speed; /* enum pci_bus_speed */ #ifdef CONFIG_PCI_DOMAINS_GENERIC int domain_nr; #endif char name[48]; unsigned short bridge_ctl; /* Manage NO_ISA/FBB/et al behaviors */ pci_bus_flags_t bus_flags; /* Inherited by child buses */ struct device *bridge; struct device dev; struct bin_attribute *legacy_io; /* Legacy I/O for this bus */ struct bin_attribute *legacy_mem; /* Legacy mem */ unsigned int is_added:1; };
在它定义里面是找不到bus_type的成员定义的,它确实不是bus_type衍生出来的。而纵观它的各项数据成员,更多的是管理层次上的作用,而不是对类型的属性阐述。这是为什么呢?借用下图,换个角度来看,这是很容易理解的,PCI作为总线出去后就是直接连接设备了。如果接口不够用,继续扩展的话,将会通过PCI桥去桥接一个PCI从总线,而不会是重新定义一个PCIx总线或者PCIy总线。硬件工程师也不容易的,何必自找麻烦设计这么多类型的总线。关于PCI总线多说一些,直接从CPU接出去的PCI总线通常被称为根总线(也称为父总线),而通过PCI桥连出去的总线则为子总线。如果编号的话呢,父总线为0,子总线为1,继而类推。具体的话,可以去看一下PCI的总线体系结构,这里不再细诉。
到此,对象的“三生万物”用下图了结。
- Sysfs设备驱动管理
Linux系统中一切皆文件。
设备文件在哪里呢?它在/dev目录下,也在/sys目录下。它们直接有什么区别呢?
/dev目录:该目录下面的文件是真实的设备文件,是应用层通过mknod创建的文件,通常系统中是由udev在运行时创建的。我们通常使用open、write、ioctl等函数操作设备,通常就是操作/dev目录下面的文件,它会间接调用到底层的驱动函数。
/sys目录:这是由内核在运行时导出的,目的就是通过文件系统展示出设备、驱动和总线等层次关系。这也是这章节的重点。
那么先通过下图看一下sysfs文件系统是如何搭建起来的,图中左边是初始化流程,右边是对应sysfs文件系统的目录结构。
可以看到sysfs并不单纯是设备驱动相关的,还包括了内核模块、内核参数以及电源等诸多管理,当然这些在某个角度上也可以视为设备或者驱动模块。不过它们不是我们分析的重点,我们着重需要分析的主要是设备驱动相关的,这图只是给了我们心中一个谱,知道它都有些什么,在哪里初始化的。
这里不准备直接通过sysfs文件目录层次结构进行分析sysfs对设备驱动的管理。我们从设备、驱动和总线等的注册添加流程进行分析sysfs文件目录层次构造的。它们主要的注册函数分别为device_register、driver_register、bus_register,还有免不了的class_register。
首先看一下device_register都做了些什么?
通过上图的device_register调用栈,可以看到设备主要的初始化都是在/sys/devices/***/下面创建一个自己名字的目录(例如xxx-device),然后在里面创建自己的属性文件接口。同时它会创建一个subsystem的链接,指向bus或者class,表示它归属的类型,挂在bus下面意味着它是由某个bus管控着,如果挂在class下面,这只是一个视角问题,其实质也是表示它具备着某些共同属性,乃至管理操作属性上的一致。然后在/sys/bus或者/sys/class下面就没有必要创建重复的东西了,直接创建一个链接指向device,意味着从它们的目录去看,可以看到bus或者class都管着哪些设备。经过device_register后,创建的目录和链接关系如下:
然后看一下driver_register都做了些什么?
driver_register如上图调用关系,可以看到它比device的注册做的事情少多了。它主要是在/sys/bus/xxx-bus/drivers目录下创建自己名字的目录,然后在里面初始化驱动属性文件。同时创建链接指向module,表示该驱动是由哪个内核模块提供功能,同样module也反向指向驱动,表示它提供的是什么样的驱动能力,当然只有驱动模块才会有指向驱动的链接。经过driver_register后,其构建的目录和链接关系如下:
接着看一下bus_register都做了什么?
通过上图可以看到,它主要是在/sys/bus目录下面创建以自己名字命名的bus目录(这里名字举例为xxx-bus),然后会创建好devices和drivers给与它管理的设备和驱动进行注册,同时创建驱动的探测drivers_probe和drivers_autoprobe属性文件,以提供给用户触发去探测。当然还少不了uevent事件文件的创建,最后还有一些bus自己特有的属性。注册完bus后,将会生成目录结构如下:
最后看一下class_register都做了些什么?
class_register的行为比较简单,仅仅是在/sys/class下面创建一个自己名字命名的目录,主要是提供给设备注册挂入链接。挂链接的动作在device_register里面完成。
至此,我们就大概对sysfs文件系统上的设备驱动注册和大概的关系逻辑了。如果通过tree命令去查看/sys肯定会发现/sys/devices目录下面的PCI、USB等设备的文件夹层层叠叠,这就涉及到了它们具体内部的一个管理关系了,这里不进行展开叙述。
系统I/O层次
1、由总线到设备
通过前面的面向对象和sysfs文件系统的目录层次关系,我们可以很清楚的知道在Linux系统中,设备和驱动是分离的。虽然说是一个萝卜一个坑,但是设备和驱动的关系不仅仅如此。同一个驱动可能会用在多个设备上面,例如多网卡机器,同样的网卡芯片类型,必然是一个驱动去操作多个网卡,只是在设备对象上做管理区分。
既然设备和驱动在软件上是分开的两个对象,那么他们结对的呢?这就是总线的功劳了。总线在软件层面主要是负责管理设备和驱动。
根据前面章节的分析,我们知道设备通过device_register让系统感知自己的存在;同样地,驱动则通过driver_register让系统感知自己的存在。如果回顾设备和驱动的对象定义,会发现设备和驱动均有bus_type类型,它指示了该设备或驱动是和什么总线关联起来的。
既然知道设备和驱动都关联到总线,那设备怎么找到最适合自己的驱动呢,或者说驱动怎么找到其所支持的设备呢?这就是由由总线负责,总线就像是一个红娘,负责在设备和驱动中牵线。
设备会向总线提出自己对驱动的条件(最简单的也是最精确的就是指定对方的名字了),而驱动也会向总线告知自己能够支持的设备的条件(一般是型号ID等,最简单的也可以是设备的名字)。
设备在注册的时候,总线就会遍历注册在它上面的驱动,找到最适合这个设备的驱动,然后填入设备的结构成员中;驱动注册的时候,总线也会遍历注册在其之上的设备,找到其支持的设备(可以是多个,驱动和设备的关系是1:N),并将设备填入驱动的支持列表中。我们称总线这个牵线的行为是match。牵好线之后,设备和驱动之间的交互红娘可不管了。
总线在匹配设备和驱动之后驱动要考虑一个这样的问题,设备对应的软件数据结构代表着静态的信息,真实的物理设备此时是否正常还不一定,因此驱动需要探测这个设备是否正常。我们称这个行为为probe,至于如何探测,那是驱动才知道干的事情,总线只管吩咐得了。
好了,回归正题,由总线到设备,这个层次感在哪里呢?我们可以通过probe行为去找到这个层次。Probe的行为发生在几种场景:一是驱动在系统中都初始化ready了,某个设备需要切换驱动的时候,我们可以通过触发/sys/bus/xxx-bus/drivers/xxx-driver/目录下的bind指定设备号去绑定,或者同目录下的rescan重新扫描;二是驱动注册的时候,它需要去检测机器中都有哪些它能够驱动的设备;三是设备注册的时候,它同样需要去找到什么驱动能够操作它。驱动注册的时候去probe,设备注册的时候也去probe,这是为何呢?这就是前面说得设备驱动分离,谁也不知道谁先初始化,其次是热插拔的设备什么时候接入并注册,这是说不好的。
我们这里挑选驱动注册的probe流程看看:
我们可以看得到去注册驱动的时候,驱动主动去探测,如果这是PCI的设备驱动,那么优先需要经过PCI总线的probe去探测,完了之后再到某个PCI设备驱动pci_drv去probe探测。这也正好展示了设备接入到系统的层层关系。
2、系统I/O分层
鸟瞰一下整个系统的I/O体系。系统I/O主要分为字符设备、网络设备和块设备,当然还有一些特殊设备,姑且将他们划分为字符设备。免得乱花渐欲迷人眼。
3、字符设备
字符设备就不展开叙述了,它毕竟比较简单,通过系统调用,经过VFS后,直接就能够调用到了file_operations里面的驱动接口了。
4、网络设备
由系统调用进入到网卡设备的操作,我们知道的就有VFS、协议层、网络层和网络设备驱动,最后才到网卡硬件。下面来看一个调用栈,非官方的分层命名。纯粹为了区分而已。
这是一个通过无线网卡发包通信的调用栈,它们都用的是同样的网卡芯片,网卡芯片使用的是ath9k。从用户态到系统的协议栈处理,这部分看客知悉便好,这部分不展开了。从协议栈下面开始,我们来看一下,先是进入到802.11无线协议,这在系统中同样体现为驱动,是作为无线驱动而存在,但是它并不体现为硬件的操作,于是它通过标准的设备操作接口去下发数据到网卡上面。然后下来到网卡驱动部分,同样的芯片,如果是PCI的话,那么CPU可以直接通过PCI映射内存地址空间进行操作,下发到硬件上。而USB不是如此,它在硬件上是CPU=>PCI=>USB host controller=>USB device=>网卡芯片这么一个曲折的流程。然后软件上的操作,它必然是先经过无线网卡驱动ath9k的处理,毕竟最终是给它对外发送的,完了才是要将它的数据信息遵循USB总线协议的规则去写到USB设备内存或者寄存器当中。
这就是网络设备上的一个软件层次关系。
5、块设备
同样的块设备也是类似网络设备这么一种方式,毕竟我们有直接PCI接出来的硬盘设备,但是我们同样有通过USB接入的硬盘设备。在遵循的设备操作流程上,也是采用了这种软件分层的方式,通过标准接口对接操作,最终实现对设备的操作。
换个角度来看,这样的分层设计能够大大地降低了开发驱动程序的成本。我们开发驱动程序的时候,我们只需要知道我们的驱动所处的位置,那么我们就知道我们可以得到什么数据,完了通过什么接口对外传递数据。
驱动要素
- 驱动的注册和注销
驱动程序的初始化函数。驱动程序的初始化在函数xxx_init()中完成,包括对硬件初始化、中断函数、向内核注册等。
首先要理解硬件结构,搞清楚其功能、接口寄存器以及CPU怎么访问控制这些寄存器等。其次要明白如何把该设备驱动注册到内核中。设备驱动程序可以直接编进内核(在移植内核时,就将该驱动程序编译进内核),在系统启动的时候初始化,也可以在需要的时候以模块的方式动态到内核中去(使用insmod加载模块到内核中,而移除模块使用rmmod卸载模块)。每个字符设备或是块设备都是通过register_chrdev()函数注册,调用该函数后就可以向系统申请主设备号,操作成功后,该设备名就会出现在/proc/devices里。
此外,在关闭设备时,需要先解除原先设备的注册,而解除注册功能在xxx_exit()中通过unregister_chrdev()函数实现,之后设备就会从/proc/devices里消失。
- 设备的访问与控制
一切皆文件,所有设备都要看成文件,并以操作文件的方式访问设备。应用程序不能直接操作硬件,而是使用统一的接口调用硬件驱动程序,这组接口被看成系统调用。每个系统调用中都有一个与之对应的函数(open、release、read、write、ioctl等),在字符驱动程序中,这些函数集合在一个file_openrations类型的数据结构中。这时候就需要根据设备的datasheet进行分析,实现对硬件的操作接口。
- 设备的轮询和中断
CPU和设备的交互通常有两种方式:轮询和中断。
很多I/O设备都有一个状态寄存器,用于描述设备当前的工作状态,每当设备状态发生改变时,设备将修改相应状态寄存器位。通过不断查询设备的状态寄存器,CPU就可以了解设备的状态,从而进行必要的I/O操作。为了节约CPU资源,查询工作往往不是连续的,而是定时进行。
轮询方式具有简单、易实现、易控制等优势,在很多小型系统中有大量应用。对那些实时敏感性不高、具有大量CPU资源的系统来说,轮询方式有很广泛的应用。最典型的用途就是在那些任务比较单一的单片机上,嵌入式系统中也有应用。
中断,顾名思义,就是打断正在进行中的工作。中断不需要处理器轮询设备的状态,设备在自己发生状态改变时将主动发送一个信号给处理器(PIC),后者在接收到这一通知信号时,会挂起当前正在执行的任务转而去处理响应外设的中断请求。中断通知机制通过硬件信号异步唤起处理器的注意,解决了外部设备与处理器之间速度不匹配导致的资源浪费问题。
现代设备绝大多数采用中断的方式与处理器进行沟通,因此设备驱动程序必须能够支持设备的中断特性。处理器在中断到达时会根据不同的中断号找到对应设备(IRR),并对中断请求进行响应处理。中断处理例程ISR(Interrupt Service Routine)由设备驱动程序提供,并在设备驱动模块初始化时注册到系统中断向量表中。从设备发出中断信号,到处理器最终调用ISR进行处理,期间会经过很多步骤,这个过程构成了中断处理框架。
模块在使用中断前要先请求一个中断通道(或者 IRQ中断请求),并在使用后释放它。通过request_irq()函数来注册中断,free_irq()函数来释放。
总结
Linux设备驱动管理框架实际上就是I/O系统框架。通过前面的分析,可以看到虽然设备驱动纷繁复杂,构成网络和文件系统的层次层层叠叠,但是都通过最简单的方式进行了处理。所有的设备、驱动和总线都是对象,只是对象的属性不同罢了,通过“一生二,二生三,三生万物”的思想,递进衍生,将其管理起来。而层层叠叠的不同层次功能,包括与硬件交互的驱动,其本质都是协议,对处于其上层和下层之间的功能起到承上启下的作用,有的简单到仅仅是做数据格式的转换,有的复杂到实现一个通信协议。而对于设备驱动的开发,必然是离不开设备的datasheet,至少需要知道怎么去操作设备吧,而对外提供的操作接口都是有现成的接口类型。如果涉及复杂层次的驱动,例如xxx无线USB网卡,那不仅仅要了解网卡设备的datasheet,免不了也需要了解对usb的操作和数据传输。总归就一句话,驱动就是承上启下的协议,对上下进行拉拢融合。