Linux设备驱动框架剖析

前提概要

      众所周知,计算机的核心是CPULinux系统就是运行在CPU之上的。但是仅仅有CPU是不够的,还需要有内存、磁盘、键盘、鼠标、显示器等各种各样的设备。Linux系统运行起来就需要将各种设备接入到计算机当中,那么他们是怎么和CPU联系在一起的呢?

通过上图我们可以看到,CPU出来后FBS总线连接北桥,北桥有PCIeMemory总线连接显卡和内存,这些都是高速数据传输设备,同时北桥还有internal总线连接南桥;南桥是作为连接慢速数据传输设备而存在的,它则通过PCILPC总线与各式设备相连接。

由此可见,CPU去操作设备,需要经过总线才能到达设备,而且到达设备会经过不同的总线。

总线是什么?维基的定义:它是指计算机组件间规范化的交换数据的方式,即以一种通用的方式为各组件提供数据传送和控制逻辑。通俗点说就是它有着特定的排线数量(总线宽度)、特定的数据速率(电平宽度)以及特定的数据格式(总线命令)的一个数据线,然后可以在这数据线上整成各式的接口,例如USB总线就可以有Type-AType-C,还可以有Mini-AMinit-B等等。

设备是什么?键盘、鼠标、显示器、打印机都是设备,这是常用的外接设备,而对于计算机而言,磁盘、显卡、声卡、光驱也都是设备,甚至内存也是设备,较为特殊的设备。总而言之,真实具体的物理器件都是设备,在软件层面,它有着自己独特的参数和属性。作为一个设备接入到计算机当中,它要有一个硬件接口接入到总线中,这个接口的线宽必然需要和总线宽度一致,而接入到总线后,要能响应总线上的命令才能够被正常使用,这就要求它要有和总线相同的速率才能识别,同时还要有和总线传输一致的数据格式。

驱动是什么?设备是多式多样的,功能更是纷繁复杂,不同总线的设备传输的数据时序和命令是不同的;即便是同总线上的不同设备虽然被总线设定好了要求的时序和命令,但是设备响应命令以及数据反馈的方式也是不同的,有的是读datasheet规定的地址上的数据,有的是读取存储的内容;甚至是同总线的同类设备,数据反馈也不一样,例如触摸屏也区分单点和多点,两者反馈是不一样的。所以驱动就是为了应对各式各样的设备,为操作设备提供操作接口。

设备驱动管理

1、 面向对象的管理

面向对象的思想就是一切事物皆对象,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;
};

对其中 ktype 的类型kobj_type进行展开:这里面有对象类型名字、管理链表等数据信息。其中parent表示父对象,诸如总线和设备就是父子关系。而sd则与sysfs的目录项进行关联,简而言之就是每一个kobject都对应着一个sysfs下面的目录。至于kref主要是对该对象的引用进行计数管理。

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);
};

展开default_groupsattribute_group,可以看到:这里面很清晰地看到它定义了一些操作接口,包括sysfs_ops这是打开sysfs文件对象后的操作接口集合。重点关注default_attrsdefault_groups,他们都是表示对象的属性信息。

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;
};

接下来看一下kset结构:这里作为属性集合,里面涵盖的attrsbin_attrs同样是表示属性集合,而且有更多的操作接口,其中bin_attrs表示模块二进制的操作接口的,它通常用在/sys/module下面,表示内核模块的属性管理,如果展开的话,它里面同样含有struct attribute结构,在设备驱动管理中基本不涉及,这里不再细诉。由此可见default_groupsdefault_attrs更全,预期未来将会替代default_attrs而存在。

struct kset {
       struct list_head list;
       spinlock_t list_lock;
       struct kobject kobj;
       const struct kset_uevent_ops *uevent_ops;
} __randomize_layout;

2)对象的“二生三”Kset表示的是对对象的一个管理集合,可以看到它拥有管理数据成员listlist_lock,这是对kobject的管理,以及uevent_ops这是对事件的响应处理函数集。同时根据kobj类型可以看到它也是一个kobject对象。正如部队里面的排长和战士的关系,排长管着一个排的兵,但是他自己同时也是一个兵。


管理的基础对象kobjectkset定义好了,那么就需要开始贴合实际,延伸出一些通用的管理类型结构。于是就有了:设备、驱动和总线,它们都将视为对象而存在。

首先看一下设备的类型是device,分析一下其定义:

再接下来看一下驱动的数据结构device_driver,它的定义:Device的结构体定义中先是看到了kobj的存在,意味着它就是一个对象,然后可以看到它还有私有数据device_private,同时还表明它处于什么总线(bus_type)下面以及由什么驱动(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;
};

struct driver_private {device_driver的结构体定义设备name名字、归属的bus总线以及支持的设备ID信息,同时还有各种操作接口,主要的有probe探测设备专用的接口和removeshutdown等等设备热插拔会用到的接口,最后肯定还少不了驱动属性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;
};

最后,总线的数据类型是bus_type,分析一下其定义:它就定义了kobject,表示驱动也是一个对象,然后除此之外还有设备链表、模块信息以及关联回去驱动的指针等。

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;
};

末了还是会有隐藏起来的kobject。它就在subsys_private结构里面。总线也是有名字的,定义了总线名字name,以及各种属性集合bus_groupsdev_groupsdrv_groups。接着还有总线的各种操作接口,比较值得关注的是matchprobe,分别用于判断设备是否与总线匹配以及对设备进行探测。多说一下,probe虽然直译是探测,实际上干得可不止那一点半点的事情,它包括了设备初始化和数据定义等一堆事情。同时应该也注意到driver也有个probe。对,他们的职能是一样的,但是如果要是总线定义有probe,那么在match设备之后,如果busprobe是定义的情况下,将会使用busprobe去初始化设备,除非busprobenull没有定义的情况下,才会使用driverprobe。为什么这么做呢?因为设备初始化的时候,往往需要有内存地址映射,将设备的缓存或者寄存器投射到内存地址空间中,如此才能通过对内存的读写去访问设备,而总线,例如PCI总线,它的设备众多,各映射各的会乱套,需要有一个掌管者去分配,它就是bus。说得多一些,即便不定义总线的probe,靠驱动的probe去做,也没什么,这就增大了驱动开发者的工作量了,同时为了避免地址冲突,也必然会增加多一个关于地址映射分配的公共模块,这会导致架构腐化了。除此之外,免不了的还有设备电源管理dev_pm_ops接口集以及内存地址映射的iommu_ops接口。

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;
};

到这里就“二生三”,衍生出了设备、驱动和总线。但是事情还没完,有些设备或者总线,它们是具备一些共同的属性或者操作接口,于是乎就新增了一个从高层次视角去抽象出来的对象,它就是class数据结构,回顾前面的总线和设备的定义,都可以看到有这么一个class这结构体定义了很多管理结构,而它作为对象的kobject则是在subsys这个kset结构体中。

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;
};

3)对象的“三生万物”同样,class具有自己的名字name、归属模块owner以及属性集合class_groupsdev_groups,还有各种接口以及电源管理操作集。同样,它的kobject是放置在了subsys_private里面,总线也是有这么一个subsys_private的。class其实就类似于面向对象的类,拥有共同的数据和属性的对象就可以共同继承这个类,虽说bus也有这么一个class,目前代码中并没有看到一个很明确的bus共同属性的class


到现在已经定义好了设备、驱动、总线和类。如何衍生系统中的设备万物呢?

设备种类纷繁复杂,我们必须挑个典型来看,否则就有可能迷失其中,那就从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设备的驱动是不是也类似这样的呢?结构虽大,而且数据信息量也不小,不过这些信息基本都是围绕着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_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;
};

至于bus_type是否也有衍生结构呢?它是没有的。那么前面pci_dev里面的pci_bus类型是什么?我们可以看一下它的定义:它定义了设备名、PCI设备管理数据以及操作接口,同时还有PCI特有的属性集合attribute_group。于是就可以看到我们关心的struct device_driver  driver。是的,它也是从device_driver衍生出来的数据类型,同样,它也类似于设备一样,有一个偏移定位结构位置的宏定义to_pci_driver。类似的USB设备驱动有usb_driverI2C设备驱动有i2c_driver

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的总线体系结构,这里不再细诉。

 到此,对象的“三生万物”用下图了结。

   

2、 Sysfs设备驱动管理

Linux系统中一切皆文件。

设备文件在哪里呢?它在/dev目录下,也在/sys目录下。它们直接有什么区别呢?

/dev目录:该目录下面的文件是真实的设备文件,是应用层通过mknod创建的文件,通常系统中是由udev在运行时创建的。我们通常使用openwriteioctl等函数操作设备,通常就是操作/dev目录下面的文件,它会间接调用到底层的驱动函数。

/sys目录:这是由内核在运行时导出的,目的就是通过文件系统展示出设备、驱动和总线等层次关系。这也是这章节的重点。

那么先通过下图看一下sysfs文件系统是如何搭建起来的,图中左边是初始化流程,右边是对应sysfs文件系统的目录结构。

可以看到sysfs并不单纯是设备驱动相关的,还包括了内核模块、内核参数以及电源等诸多管理,当然这些在某个角度上也可以视为设备或者驱动模块。不过它们不是我们分析的重点,我们着重需要分析的主要是设备驱动相关的,这图只是给了我们心中一个谱,知道它都有些什么,在哪里初始化的。

这里不准备直接通过sysfs文件目录层次结构进行分析sysfs对设备驱动的管理。我们从设备、驱动和总线等的注册添加流程进行分析sysfs文件目录层次构造的。它们主要的注册函数分别为device_registerdriver_registerbus_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),然后会创建好devicesdrivers给与它管理的设备和驱动进行注册,同时创建驱动的探测drivers_probedrivers_autoprobe属性文件,以提供给用户触发去探测。当然还少不了uevent事件文件的创建,最后还有一些bus自己特有的属性。注册完bus后,将会生成目录结构如下:

最后看一下class_register都做了些什么?

class_register的行为比较简单,仅仅是在/sys/class下面创建一个自己名字命名的目录,主要是提供给设备注册挂入链接。挂链接的动作在device_register里面完成。

至此,我们就大概对sysfs文件系统上的设备驱动注册和大概的关系逻辑了。如果通过tree命令去查看/sys肯定会发现/sys/devices目录下面的PCIUSB等设备的文件夹层层叠叠,这就涉及到了它们具体内部的一个管理关系了,这里不进行展开叙述。

系统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_drvprobe探测。这也正好展示了设备接入到系统的层层关系。

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接入的硬盘设备。在遵循的设备操作流程上,也是采用了这种软件分层的方式,通过标准接口对接操作,最终实现对设备的操作。

换个角度来看,这样的分层设计能够大大地降低了开发驱动程序的成本。我们开发驱动程序的时候,我们只需要知道我们的驱动所处的位置,那么我们就知道我们可以得到什么数据,完了通过什么接口对外传递数据。

驱动要素

1、 驱动的注册和注销

驱动程序的初始化函数。驱动程序的初始化在函数xxx_init()中完成,包括对硬件初始化、中断函数、向内核注册等。

首先要理解硬件结构,搞清楚其功能、接口寄存器以及CPU怎么访问控制这些寄存器等。其次要明白如何把该设备驱动注册到内核中。设备驱动程序可以直接编进内核(在移植内核时,就将该驱动程序编译进内核),在系统启动的时候初始化,也可以在需要的时候以模块的方式动态到内核中去(使用insmod加载模块到内核中,而移除模块使用rmmod卸载模块)。每个字符设备或是块设备都是通过register_chrdev()函数注册,调用该函数后就可以向系统申请主设备号,操作成功后,该设备名就会出现在/proc/devices里。

此外,在关闭设备时,需要先解除原先设备的注册,而解除注册功能在xxx_exit()中通过unregister_chrdev()函数实现,之后设备就会从/proc/devices里消失。

2、 设备的访问与控制

一切皆文件,所有设备都要看成文件,并以操作文件的方式访问设备。应用程序不能直接操作硬件,而是使用统一的接口调用硬件驱动程序,这组接口被看成系统调用。每个系统调用中都有一个与之对应的函数(openreleasereadwriteioctl等),在字符驱动程序中,这些函数集合在一个file_openrations类型的数据结构中。这时候就需要根据设备的datasheet进行分析,实现对硬件的操作接口。

3、 设备的轮询和中断

CPU和设备的交互通常有两种方式:轮询和中断。

很多I/O设备都有一个状态寄存器,用于描述设备当前的工作状态,每当设备状态发生改变时,设备将修改相应状态寄存器位。通过不断查询设备的状态寄存器,CPU就可以了解设备的状态,从而进行必要的I/O操作。为了节约CPU资源,查询工作往往不是连续的,而是定时进行。 

轮询方式具有简单、易实现、易控制等优势,在很多小型系统中有大量应用。对那些实时敏感性不高、具有大量CPU资源的系统来说,轮询方式有很广泛的应用。最典型的用途就是在那些任务比较单一的单片机上,嵌入式系统中也有应用。 

中断,顾名思义,就是打断正在进行中的工作。中断不需要处理器轮询设备的状态,设备在自己发生状态改变时将主动发送一个信号给处理器(PIC),后者在接收到这一通知信号时,会挂起当前正在执行的任务转而去处理响应外设的中断请求。中断通知机制通过硬件信号异步唤起处理器的注意,解决了外部设备与处理器之间速度不匹配导致的资源浪费问题。 

现代设备绝大多数采用中断的方式与处理器进行沟通,因此设备驱动程序必须能够支持设备的中断特性。处理器在中断到达时会根据不同的中断号找到对应设备(IRR),并对中断请求进行响应处理。中断处理例程ISRInterrupt Service Routine)由设备驱动程序提供,并在设备驱动模块初始化时注册到系统中断向量表中。从设备发出中断信号,到处理器最终调用ISR进行处理,期间会经过很多步骤,这个过程构成了中断处理框架。

模块在使用中断前要先请求一个中断通道(或者 IRQ中断请求),并在使用后释放它。通过request_irq()函数来注册中断,free_irq()函数来释放。

总结

    Linux设备驱动管理框架实际上就是I/O系统框架。通过前面的分析,可以看到虽然设备驱动纷繁复杂,构成网络和文件系统的层次层层叠叠,但是都通过最简单的方式进行了处理。所有的设备、驱动和总线都是对象,只是对象的属性不同罢了,通过“一生二,二生三,三生万物”的思想,递进衍生,将其管理起来。而层层叠叠的不同层次功能,包括与硬件交互的驱动,其本质都是协议,对处于其上层和下层之间的功能起到承上启下的作用,有的简单到仅仅是做数据格式的转换,有的复杂到实现一个通信协议。而对于设备驱动的开发,必然是离不开设备的datasheet,至少需要知道怎么去操作设备吧,而对外提供的操作接口都是有现成的接口类型。如果涉及复杂层次的驱动,例如xxx无线USB网卡,那不仅仅要了解网卡设备的datasheet,免不了也需要了解对usb的操作和数据传输。总归就一句话,驱动就是承上启下的协议,对上下进行拉拢融合。

发表评论

电子邮件地址不会被公开。 必填项已用*标注