linux设备驱动——bus、device、driver加载顺序与匹配流程

1. 前言

最近回看了下Linux设备驱动相关知识,做了个总结。有些话需要说在前面:

  • 文中有些内容为个人理解(上标H所标识内容),未必准确,有误请评论指正。
  • 4.2节的内容主要目的是为了搞清楚driver和device在加载的过程中是如何通过bus相互匹配。

本文源码源自4.10.17版本linux内核

2. 概念

Linux设备驱动有三个基本概念:总线驱动以及设备 三者之间关 系 H 三者之间关系^H 三者之间关H简单描述如下:

  1. 总线为外设或片内模组与核心处理器之间的通信总线,例如SPI、I2C、SDIO、USB等。
  2. 每个驱动、设备都需要挂载到一种总线上;
  3. 挂载到相同总线的驱动和设备可以通过该总线进行匹配,实现设备与对应的驱动之间的绑定。

2.1. 数据结构

以下为总线、驱动及设备在linux内核中对应的核心数据结构

// 总线数据结构 -- include/linux/device.h
struct bus_type {
	const char		*name;
	const char		*dev_name;
	struct device		*dev_root;
	struct device_attribute	*dev_attrs;	/* use dev_groups instead */
	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);

	const struct dev_pm_ops *pm;

	const struct iommu_ops *iommu_ops;

	struct subsys_private *p;	// 如下
	struct lock_class_key lock_key;
};
// drivers/base/base.h
struct subsys_private {
	struct kset subsys;			// the struct kset that defines this subsystem
	struct kset *devices_kset;	// the subsystem's 'devices' directory
	struct list_head interfaces;
	struct mutex mutex;

	struct kset *drivers_kset;	// the list of drivers associated
	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;
};
// 驱动数据结构 -- include/linux/device.h
struct device_driver {
	const char		*name;
	struct bus_type		*bus;	// 指向本驱动挂载的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 dev_pm_ops *pm;

	struct driver_private *p;	// 如下
};
// drivers/base/base.h
struct driver_private {
	struct kobject kobj;
	struct klist klist_devices;
	struct klist_node knode_bus;
	struct module_kobject *mkobj;
	struct device_driver *driver;
};
// 设备数据结构 -- include/linux/device.h
struct device {
	struct device		*parent;

	struct device_private	*p;

	struct kobject kobj;
	const char		*init_name; /* initial name of the device */
	const struct device_type *type;

	struct mutex		mutex;	/* mutex to synchronize calls to
					 * its driver.
					 */

	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/get_drvdata */
	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

#ifdef CONFIG_NUMA
	int		numa_node;	/* NUMA node this device is close to */
#endif
	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. */
	unsigned long	dma_pfn_offset;

	struct device_dma_parameters *dma_parms;

	struct list_head	dma_pools;	/* dma pools (if dma'ble) */

	struct dma_coherent_mem	*dma_mem; /* internal for coherent mem
					     override */
#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 */

	dev_t			devt;	/* dev_t, creates the sysfs "dev" */
	u32			id;	/* device instance */

	spinlock_t		devres_lock;
	struct list_head	devres_head;

	struct klist_node	knode_class;
	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;

	bool			offline_disabled:1;
	bool			offline:1;
};

2.2. probe函数

bus_type和device_driver中都包含probe()函数,两者关系可追踪到函数really_probe()中(该函数在后续的调用加载流程中会描述具体执行的位置,可先跳过,后续查看加载流程时返回此处查看),如下:

static int really_probe(struct device *dev, struct device_driver *drv)
{
	int ret = -EPROBE_DEFER;
	int local_trigger_count = atomic_read(&deferred_trigger_count);
	bool test_remove = IS_ENABLED(CONFIG_DEBUG_TEST_DRIVER_REMOVE) &&
			   !drv->suppress_bind_attrs;

	...

	atomic_inc(&probe_count);
	pr_debug("bus: '%s': %s: probing driver %s with device %s\n",
		 drv->bus->name, __func__, drv->name, dev_name(dev));
	WARN_ON(!list_empty(&dev->devres_head));

re_probe:
	dev->driver = drv;

	/* If using pinctrl, bind pins now before probing */
	ret = pinctrl_bind_pins(dev);
	if (ret)
		goto pinctrl_bind_failed;

	if (driver_sysfs_add(dev)) {
		printk(KERN_ERR "%s: driver_sysfs_add(%s) failed\n",
			__func__, dev_name(dev));
		goto probe_failed;
	}

	if (dev->pm_domain && dev->pm_domain->activate) {
		ret = dev->pm_domain->activate(dev);
		if (ret)
			goto probe_failed;
	}

	/*
	 * Ensure devices are listed in devices_kset in correct order
	 * It's important to move Dev to the end of devices_kset before
	 * calling .probe, because it could be recursive and parent Dev
	 * should always go first
	 */
	devices_kset_move_last(dev);

	/*****  以下为重点  ****************/
	if (dev->bus->probe) {		// 若bus_type中定义了probe,则调用bus_type的probe
		ret = dev->bus->probe(dev);
		if (ret)
			goto probe_failed;
	} else if (drv->probe) {	// 若bus_type中未定义了probe,则调用bus_type的probe
		ret = drv->probe(dev);
		if (ret)
			goto probe_failed;
	}
	// 总结:在看过的有限几个总线源码里,bus_type.probe()最终依然会调用device_driver.probe()
	//      个人理解 —— device_driver.probe()是必然会调用的,
	//                 添加bus_type.probe()是因为有的总线需要在device_driver.probe()之前做些额外的处理
	/*****  重点已结束  ****************/
	
	...

	pinctrl_init_done(dev);

	if (dev->pm_domain && dev->pm_domain->sync)
		dev->pm_domain->sync(dev);

	driver_bound(dev);
	ret = 1;
	pr_debug("bus: '%s': %s: bound device %s to driver %s\n",
		 drv->bus->name, __func__, dev_name(dev), drv->name);
	goto done;

probe_failed:
	if (dev->bus)
		blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
					     BUS_NOTIFY_DRIVER_NOT_BOUND, dev);
pinctrl_bind_failed:
	...
	ret = 0;
done:
	atomic_dec(&probe_count);
	wake_up(&probe_waitqueue);
	return ret;
}

3. bus、device、driver加载顺序

3.1. 加载方式


bus加载方式

以SPI总线为例(drivers/spi/spi.c),初始化函数加载调用统一使用postcore_initcall()将总线的初始化接口spi_init()添加到内核的启动序列中,跟踪一下该函数:

postcore_initcall(spi_init)	// drivers/spi/spi.c
-> #define postcore_initcall(fn) __define_initcall(fn, 2)	// include/linux/init.h

driver加载方式——动态加载、静态加载

驱动加载有两种方式:

  • 静态加载:驱动随着linux内核的启动自动加载
  • 动态加载:linux系统启动后,使用insmod命令加载

本节讨论的加载顺序为linux系统启动过程中的加载顺序,因此只考虑静态加载的情况。

驱动静态加载方式与总线类似,也是调用了统一的接口将驱动的初始化函数添加到内核的启动序列中。以基于SPI总线的驱动rtc-pcf2123(drivers/rtc/rtc-pcf2123.c)为例,驱动加载使用的统一接口为module_init()。下面代码可以示意驱动如何调用该函数,以及该函数如何将驱动初始化接口添加到内核启动序列中。

module_spi_driver(pcf2123_driver)	// drivers/rtc/rtc-pcf2123.c

-> #define module_spi_driver(__spi_driver) \	// include/linux/spi/spi.h
	       module_driver(__spi_driver, spi_register_driver, \
			             spi_unregister_driver)

   -> #define module_driver(__driver, __register, __unregister, ...) \	//include/linux/device.h
	  static int __init __driver##_init(void) \	// 对rtc-pcf2123驱动,这里定义了初始化函数pcf2123_driver_init()
	  { \
		  return __register(&(__driver) , ##__VA_ARGS__); \	// 对rtc-pcf2123驱动,这里调用了接口spi_register_driver(&(pcf2123_driver))
	  } \
	  module_init(__driver##_init); \	
	  static void __exit __driver##_exit(void) \
	  { \
		  __unregister(&(__driver) , ##__VA_ARGS__); \
	  } \
	  module_exit(__driver##_exit);

      -> #define module_init(x)	__initcall(x);	// include/linux/module.h
      
         -> #define __initcall(fn) device_initcall(fn)	// include/linux/init.h
           
            -> #define device_initcall(fn) __define_initcall(fn, 6)	// include/linux/init.h         

device加载方式——platform

linux内核使用devicetree描述具体硬件平台的设备参数,platform平台通过调用接口of_platform_default_populate_init(),将devicetree中的设备节点转换为内核可识别的platform设备数据结构platform_device,该结构中包含device结构。
linux内核启动后,调用arch_initcall_sync()接口将of_platform_default_populate_init()添加到启动序列中。跟踪该函数:

arch_initcall_sync(of_platform_default_populate_init)	// drivers/of/platform.c
-> #define arch_initcall_sync(fn) __define_initcall(fn, 3s)	// include/linux/init.h

3.2. 加载顺序

从bus、device、driver的加载方式可以看到,三者的初始化都调用了相同的接口__define_initcall(fn, id),函数定义及功能说明如下:

//	include/linux/init.h
#define __define_initcall(fn, id) \	
        static initcall_t __initcall_##fn##id __used \	
		__attribute__((__section__(".initcall" #id ".init"))) = fn;	
/*************************/
// 功能说明
// 1. 定义名为__initcall_##fn##id的函数指针,指向fn函数
// 2. 将该函数指针存放在.initcall#id.init段中
// 3. .initcall#id.init段在内核初始化的过程中会被调用。
//    其中,id越小的.initcall#id.init段内的接口越早被调用

对该接口有兴趣,可以参照Reference2的详细说明。

由于bus、device、driver的id分别为2、3s、6,因此,初始化的顺序依次为:bus、device、driver

4. device、driver匹配流程

此节主要想讨论device和driver是如何找到可以匹配的彼此。因此,分别从driver和device两个方向,简述从初始化到调用probe()完成driver与device的匹配的过程。
首先做个说明,从后续描述将会看到,driver和device都挂载在总线上,因此二者向找到相互匹配的彼此,都需要通过bus这一媒介

4.1. 加载driver

driver还是以rtc-pcf2123驱动为例,从驱动的初始化函数pcf2123_driver_init()开始,调用流程如下:

pcf2123_driver_init()	
-> spi_register_driver(&(pcf2123_driver))	// 前两步参照3.2节的代码说明吧
   -> __spi_register_driver(THIS_MODULE, &(pcf2123_driver))	// include/linux/spi/spi.h
      -> driver_register(&(&pcf2123_driver)->driver)	// include/linux/spi/spi.c
         -> bus_add_driver(&(&pcf2123_driver)->driver)	// drivers/base/driver.c
         												// 重要地bus媒介在这里
            -> driver_attach(&(&pcf2123_driver)->driver)	// drivers/base/bus.c
               ->  __driver_attach(dev, &(&pcf2123_driver)->driver)	// drivers/base/dd.c
                                                                    // 该函数被循环调用,将&(&pcf2123_driver)->driver驱动
                                                                    // 依次与spi总线上已挂载的每个设备dev进行匹配
                  -> driver_probe_device(&(&pcf2123_driver)->driver, dev)	// drivers/base/dd.c
                     -> really_probe(dev, &(&pcf2123_driver)->driver)	// drivers/base/dd.c
                     													// 可以参照2.2节中的接口说明
/***************************/
// 到此,该驱动完成初始化;且若总线上已经挂在了可匹配的设备,则完成匹配

4.2. 加载device

由于积累有限,还无法完全理解platform平台。因此, 草率地将 l i n u x 设备分为两 种 H 草率地将linux设备分为两种^H 草率地将linux设备分为两H(命名只是为了区分我的这两种分类,非官方命名)

  • 总线设备:挂载与SPI、I2C等总线的设备
  • platform设备:挂载与platform总线的设备

两种设备初始化使用不同的处理流程,但是最终都会调用同一个接口device_add(),下面先介绍两种设备从初始化到device_add()接口的调用流程。


总线设备device_add()前流程

以I2C设备为例,设备初始化从接口i2c_new_device()开始,接口调用流程如下:

i2c_new_device(i2c_adapter *adap)	// drivers/i2c/i2c-core.c
-> device_register(struct device *dev)	// drivers/base/core.c
   -> device_add(dev)	// drivers/base/core.c

platform设备device_add()前流程

platform设备就从初始化设备树节点的of_platform_default_populate_init()开始,接口调用流程如下:

of_platform_default_populate_init()	// drivers/of/platform.c
-> of_platform_default_populate()	// drivers/of/platform.c
   -> of_platform_populate()	// drivers/of/platform.c
      -> of_platform_bus_create()	// drivers/of/platform.c
                                    // 遍历devicetree的设备节点,每个节点调用一次该函数
                                    // 函数为自己及其children创建platform设备
         -> of_platform_device_create_pdata()	// drivers/of/platform.c
            -> of_device_add()	// drivers/of/device.c
               -> device_add()	// drivers/base/core.c
                  -> bus_add_device() // drivers/base/bus.c
                                      // 完成设备向bus的添加

device与driver的匹配

上节device_add()调用bus_add_device()将设备添加到了bus的设备列表中。继续调用bus_probe_device(),已完成deice与driver的绑定。具体流程如下,设备从调用device_add()probe()接口完成device与driver的匹配。

device_add()	// drivers/base/core.c
-> bus_probe_device(dev)	// drivers/base/bus.c
      						// 重要的bus媒介出现了
   -> device_initial_probe(dev)	// drivers/base/bus.c
                                // 说句实话,这里没有经过运行验证,感觉应该走这个函数对应的分支
      -> __device_attach(dev, true)	// drivers/base/dd.c
          -> driver_probe_device(drv, dev)	// drivers/base/dd.c
             -> really_probe(dev, drv)	// drivers/base/dd.c
                     					// 可以参照2.2节中的接口说明
/***************************/
// 到此,该设备完成初始化;且若总线上已经挂载了可匹配的驱动,则完成匹配

5. Reference

  1. 《学习笔记——《LINUX设备驱动程序(第三版)》Linux设备模型:内核添加、删除设备、驱动程序》
  2. module_init机制的理解
  3. 内核对设备树的处理(四)__device_node转换为platform_device
  • 6
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
内容: 一. Bootloader 二.Kernel引导入口 三.核心数据结构初始化--内核引导第一部分 四.外设初始化--内核引导第二部分 五.init进程和inittab引导指令 六.rc启动脚本 七.getty和login 八.bash 附:XDM方式登录 本文以Redhat 6.0 Linux 2.2.19 for Alpha/AXP为平台,描述了从开机到登录的 Linux 启动全过程。该文对i386平台同样适用。 一. Bootloader 在Alpha/AXP 平台上引导Linux通常有两种方法,一种是由MILO及其他类似的引导程序引导,另一种是由Firmware直接引导。MILO功能与i386平台的LILO相近,但内置有基本的磁盘驱动程序(如IDE、SCSI等),以及常见的文件系统驱动程序(如ext2,iso9660等), firmware有ARC、SRM两种形式,ARC具有类BIOS界面,甚至还有多重引导的设置;而SRM则具有功能强大的命令行界面,用户可以在控制台上使用boot等命令引导系统。ARC有分区(Partition)的概念,因此可以访问到分区的首扇区;而SRM只能将控制转给磁盘的首扇区。两种firmware都可以通过引导MILO来引导Linux,也可以直接引导Linux的引导代码。 “arch/alpha/boot” 下就是制作Linux Bootloader的文件。“head.S”文件提供了对 OSF PAL/1的调用入口,它将被编译后置于引导扇区(ARC的分区首扇区或SRM的磁盘0扇区),得到控制后初始化一些数据结构,再将控制转给“main.c”中的start_kernel(), start_kernel()向控制台输出一些提示,调用pal_init()初始化PAL代码,调用openboot() 打开引导设备(通过读取Firmware环境),调用load()将核心代码加载到START_ADDR(见 “include/asm-alpha/system.h”),再将Firmware中的核心引导参数加载到ZERO_PAGE(0) 中,最后调用runkernel()将控制转给0x100000的kernel,bootloader部分结束。 “arch/alpha/boot/bootp.c”以“main.c”为基础,可代替“main.c”与“head.S” 生成用于BOOTP协议网络引导的Bootloader。 Bootloader中使用的所有“srm_”函数在“arch/alpha/lib/”中定义。 以上这种Boot方式是一种最简单的方式,即不需其他工具就能引导Kernel,前提是按照 Makefile的指导,生成bootimage文件,内含以上提到的bootloader以及vmlinux,然后将 bootimage写入自磁盘引导扇区始的位置中。 当采用MILO这样的引导程序来引导Linux时,不需要上面所说的Bootloader,而只需要 vmlinux或vmlinux.gz,引导程序会主动解压加载内核到0x1000(小内核)或0x100000(大内核),并直接进入内核引导部分,即本文的第二节。 对于I386平台 i386系统中一般都有BIOS做最初的引导工作,那就是将四个主分区表中的第一个可引导 分区的第一个扇区加载到实模式地址0x7c00上,然后将控制转交给它。 在“arch/i386/boot” 目录下,bootsect.S是生成引导扇区的汇编源码,它首先将自己拷贝到0x90000上,然后将紧接其后的setup部分(第二扇区)拷贝到0x90200,将真正的内核代码拷贝到0x100000。以上这些拷贝动作都是以bootsect.S、setup.S以及vmlinux在磁盘上连续存放为前提的,也就是说,我们的bzImage文件或者zImage文件是按照bootsect,setup, vmlinux这样的顺序组织,并存放于始于引导分区的首扇区的连续磁盘扇区之中。 bootsect.S完成加载动作后,就直接跳转到0x90200,这里正是setup.S的程序入口。 setup.S的主要功能就是将系统参数(包括内存、磁盘等,由BIOS返回)拷贝到 0x90000-0x901FF内存中,这个地方正是bootsect.S存放的地方,这时它将被系统参数覆盖。以后这些参数将由保护模式下的代码来读取。 除此之外,setup.S还将video.S中的代码包含进来,检测和设置显示器和显示模式。最 后,setup.S将系统转换到保护模式,并跳转到0x100000(对于bzImage格式的大内核是 0x100000,对于zImage格式的是0x1000)的内核引导代码,Bootloader过程结束。 对于2.4.x版内核 没有什么变化。 二.Kernel引导入口 在arch/alpha/vmlinux.lds 的链接脚本控制下,链接程序将vmlinux的入口置于 "arch/alpha/kernel/head.S"中的__start上,因此当Bootloader跳转到0x100000时, __start处的代码开始执行。__start的代码很简单,只需要设置一下全局变量,然后就跳转到start_kernel去了。start_kernel()是"init/main.c"中的asmlinkage函数,至此,启动过程转入体系结构无关的通用C代码中。 对于I386平台 在i386体系结构中,因为i386本身的问题,在 "arch/alpha/kernel/head.S"中需要更多的设置,但最终也是通过call SYMBOL_NAME(start_kernel)转到start_kernel()这个体系结构无关的函数中去执行了。 所不同的是,在i386系统中,当内核以bzImage的形式压缩,即大内核方式(__BIG_KERNEL__)压缩时就需要预先处理bootsect.S和setup.S,按照大核模式使用$(CPP) 处理生成bbootsect.S和bsetup.S,然后再编译生成相应的.o文件,并使用 "arch/i386/boot/compressed/build.c"生成的build工具,将实际的内核(未压缩的,含 kernel中的head.S代码)与"arch/i386/boot/compressed"下的head.S和misc.c合成到一起,其中的 head.S代替了"arch/i386/kernel/head.S"的位置,由Bootloader引导执行(startup_32入口),然后它调用misc.c中定义的decompress_kernel()函数,使用 "lib/inflate.c"中定义的gunzip()将内核解压到0x100000,再转到其上执行 "arch/i386/kernel/head.S"中的startup_32代码。 对于2.4.x版内核 没有变化。 三.核心数据结构初始化--内核引导第一部分 start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。 这些动作有的是公共的,有的则是需要配置的才会执行的。 在start_kernel()函数中, 输出Linux版本信息(printk(linux_banner)) 设置与体系结构相关的环境(setup_arch()) 页表结构初始化(paging_init()) 使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init()) 使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ()) 核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init()) 时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init()) 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options()) 控制台初始化(为输出信息而先于PCI初始化,console_init()) 剖析器数据结构初始化(prof_buffer和prof_len变量) 核心Cache初始化(描述Cache信息的Cache,kmem_cache_init()) 延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay()) 内存初始化(设置内存上下界和页表项初始值,mem_init()) 创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init()) 创建uid taskcount SLAB cache("uid_cache",uidcache_init()) 创建文件cache("files_cache",filescache_init()) 创建目录cache("dentry_cache",dcache_init()) 创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init()) 块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init()) 创建页cache(内存页hash表初始化,page_cache_init()) 创建信号队列cache("signal_queue",signals_init()) 初始化内存inode表(inode_init()) 创建内存文件描述符表("filp_cache",file_table_init()) 检查体系结构漏洞(对于alpha,此函数为空,check_bugs()) SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init()) 启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init()) 至此start_kernel()结束,基本的核心环境已经建立起来了。 对于I386平台 i386平台上的内核启动过程与此基本相同,所不同的主要是实现方式。 对于2.4.x版内核 2.4.x中变化比较大,但基本过程没变,变动的是各个数据结构的具体实现,比如Cache。 四.外设初始化--内核引导第二部分 init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下: 总线初始化(比如pci_init()) 网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init()) 创建bdflush核心线程(bdflush()过程常驻核心空间,由核心唤醒来清理被写过的内存缓冲区,当bdflush()由kernel_thread()启动后,它将自己命名为kflushd) 创建kupdate核心线程(kupdate()过程常驻核心空间,由核心按时调度执行,将内存缓冲区中的信息更新到磁盘中,更新的内容包括超级块和inode表) 设置并启动核心调页线程kswapd(为了防止kswapd启动时将版本信息输出到其他信息中间,核心线调用kswapd_setup()设置kswapd运行所要求的环境,然后再创建 kswapd核心线程) 创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为keventd) 设备初始化(包括并口parport_init()、字符设备chr_dev_init()、块设备 blk_dev_init()、SCSI设备scsi_dev_init()、网络设备net_dev_init()、磁盘初始化及分区检查等等, device_setup()) 执行文件格式设置(binfmt_setup()) 启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls()) 文件系统初始化(filesystem_setup()) 安装root文件系统(mount_root()) 至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开 /dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。 init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体: start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现 init线程,由start_kernel()创建,当前处于用户态,加载了init程序 kflushd核心线程,由init线程创建,在核心态运行bdflush()函数 kupdate核心线程,由init线程创建,在核心态运行kupdate()函数 kswapd核心线程,由init线程创建,在核心态运行kswapd()函数 keventd核心线程,由init线程创建,在核心态运行context_thread()函数 对于I386平台 基本相同。 对于2.4.x版内核 这一部分的启动过程在2.4.x内核中简化了不少,缺省的独立初始化过程只剩下网络 (sock_init())和创建事件管理核心线程,而其他所需要的初始化都使用__initcall()宏 包含在do_initcalls()函数中启动执行。 五.init进程和inittab引导指令 init进程是系统所有进程的起点,内核在完成核内引导以后,即在本线程(进程)空 间内加载init程序,它的进程号是1。 init程序需要读取/etc/inittab文件作为其行为指针,inittab是以行为单位的描述性(非执行性)文本,每一个指令行都具有以下格式: id:runlevel:action:process其中id为入口标识符,runlevel为运行级别,action为动作代号,process为具体的执行程序。 id一般要求4个字符以内,对于getty或其他login程序项,要求id与tty的编号相同,否则getty程序将不能正常工作。 runlevel 是init所处于的运行级别的标识,一般使用0-6以及S或s。0、1、6运行级别被系统保留,0作为shutdown动作,1作为重启至单用户模式,6 为重启;S和s意义相同,表示单用户模式,且无需inittab文件,因此也不在inittab中出现,实际上,进入单用户模式时,init直接在控制台(/dev/console)上运行/sbin/sulogin。 在一般的系统实现中,都使用了2、3、4、5几个级别,在 Redhat系统中,2表示无NFS支持的多用户模式,3表示完全多用户模式(也是最常用的级别),4保留给用户自定义,5表示XDM图形登录方式。7- 9级别也是可以使用的,传统的Unix系统没有定义这几个级别。runlevel可以是并列的多个值,以匹配多个运行级别,对大多数action来说,仅当runlevel与当前运行级别匹配成功才会执行。 initdefault是一个特殊的action值,用于标识缺省的启动级别;当init由核心激活 以后,它将读取inittab中的initdefault项,取得其中的runlevel,并作为当前的运行级别。如果没有inittab文件,或者其中没有initdefault项,init将在控制台上请求输入 runlevel。 sysinit、 boot、bootwait等action将在系统启动时无条件运行,而忽略其中的runlevel,其余的action(不含initdefault)都与某个runlevel相关。各个action的定义在inittab的man手册中有详细的描述。 在Redhat系统中,一般情况下inittab都会有如下几项: id:3:initdefault: #表示当前缺省运行级别为3--完全多任务模式; si::sysinit:/etc/rc.d/rc.sysinit #启动时自动执行/etc/rc.d/rc.sysinit脚本 l3:3:wait:/etc/rc.d/rc 3 #当运行级别为3时,以3为参数运行/etc/rc.d/rc脚本,init将等待其返回 0:12345:respawn:/sbin/mingetty tty0 #在1-5各个级别上以tty0为参数执行/sbin/mingetty程序,打开tty0终端用于 #用户登录,如果进程退出则再次运行mingetty程序 x:5:respawn:/usr/bin/X11/xdm -nodaemon #在5级别上运行xdm程序,提供xdm图形方式登录界面,并在退出时重新执行. 六.rc启动脚本 上一节已经提到init进程将启动运行rc脚本,这一节将介绍rc脚本具体的工作。 一般情况下,rc启动脚本都位于/etc/rc.d目录下,rc.sysinit中最常见的动作就是激活交换分区,检查磁盘,加载硬件模块,这些动作无论哪个运行级别都是需要优先执行的。仅当rc.sysinit执行完以后init才会执行其他的boot或bootwait动作。 如果没有其他boot、bootwait动作,在运行级别3下,/etc/rc.d/rc将会得到执行,命令行参数为3,即执行 /etc/rc.d/rc3.d/目录下的所有文件。rc3.d下的文件都是指向/etc/rc.d/init.d/目录下各个Shell脚本的符号连接,而这些脚本一般能接受start、stop、restart、status等参数。rc脚本以start参数启动所有以S开头的脚本,在此之前,如果相应的脚本也存在K打头的链接,而且已经处于运行态了(以/var/lock/subsys/下的文件作为标志),则将首先启动K开头的脚本,以stop 作为参数停止这些已经启动了的服务,然后再重新运行。显然,这样做的直接目的就是当init改变运行级别时,所有相关的服务都将重启,即使是同一个级别。 rc程序执行完毕后,系统环境已经设置好了,下面就该用户登录系统了。 七.getty和login 在rc返回后,init将得到控制,并启动mingetty(见第五节)。mingetty是getty的简化,不能处理串口操作。getty的功能一般包括: 打开终端线,并设置模式 输出登录界面及提示,接受用户名的输入 以该用户名作为login的参数,加载login程序 缺省的登录提示记录在/etc/issue文件中,但每次启动,一般都会由rc.local脚本根据系统环境重新生成。 注:用于远程登录的提示信息位于/etc/issue.net中。 login程序在getty的同一个进程空间中运行,接受getty传来的用户名参数作为登录的用户名。 如果用户名不是root,且存在/etc/nologin文件,login将输出nologin文件的内容,然后退出。这通常用来系统维护时防止非root用户登录。 只有/etc/securetty中登记了的终端才允许root用户登录,如果不存在这个文件,则root可以在任何终端上登录。/etc/usertty文件用于对用户作出附加访问限制,如果不存在这个文件,则没有其他限制。 当用户登录通过了这些检查后,login将搜索/etc/passwd文件(必要时搜索 /etc/shadow文件)用于匹配密码、设置主目录和加载shell。如果没有指定主目录,将默认为根目录;如果没有指定shell,将默认为 /bin/sh。在将控制转交给shell以前, getty将输出/var/log/lastlog中记录的上次登录系统的信息,然后检查用户是否有新邮件(/usr/spool/mail/ {username})。在设置好shell的uid、gid,以及TERM,PATH 等环境变量以后,进程加载shell,login的任务也就完成了。 八.bash 运行级别3下的用户login以后,将启动一个用户指定的shell,以下以/bin/bash为例继续我们的启动过程。 bash 是Bourne Shell的GNU扩展,除了继承了sh的所有特点以外,还增加了很多特性和功能。由login启动的bash是作为一个登录shell启动的,它继承了 getty设置的TERM、PATH等环境变量,其中PATH对于普通用户为"/bin:/usr/bin:/usr/local/bin",对于 root 为"/sbin:/bin:/usr/sbin:/usr/bin"。作为登录shell,它将首先寻找/etc/profile 脚本文件,并执行它;然后如果存在~/.bash_profile,则执行它,否则执行 ~/.bash_login,如果该文件也不存在,则执行~/.profile文件。然后bash将作为一个交互式shell执行~/.bashrc文件(如果存在的话),很多系统中,~/.bashrc都将启动 /etc/bashrc作为系统范围内的配置文件。 当显示出命令行提示符的时候,整个启动过程就结束了。此时的系统,运行着内核,运行着几个核心线程,运行着init进程,运行着一批由rc启动脚本激活的守护进程(如 inetd等),运行着一个bash作为用户的命令解释器。 附:XDM方式登录 如果缺省运行级别设为5,则系统中不光有1-6个getty监听着文本终端,还有启动了一个XDM的图形登录窗口。登录过程和文本方式差不多,也需要提供用户名和口令,XDM 的配置文件缺省为/usr/X11R6/lib/X11/xdm/xdm-config文件,其中指定了 /usr/X11R6/lib/X11/xdm/xsession作为XDM的会话描述脚本。登录成功后,XDM将执行这个脚本以运行一个会话管理器,比如gnome-session等。 除了XDM以外,不同的窗口管理系统(如KDE和GNOME)都提供了一个XDM的替代品,如gdm和kdm,这些程序的功能和XDM都差不多。
### 回答1: CAN(Controller Area Network)总线是一种用于实时通信的串行总线系统。它最初是由德国Bosch公司开发,用于汽车电子系统中的通信。CAN总线采用差分信号传输数据,具有高抗干扰性和可靠性。 CAN总线采用了分布式控制和多主机的工作方式。每个节点都可以发送和接收消息,节点之间通过CAN总线进行通信。CAN总线具有高度的可扩展性,可以支持多达100个节点的连接。 CAN总线的主要特点是高速通信和优秀的容错性能。它可以在最高1 Mbps的速度下进行通信,能够满足复杂的实时应用需求。此外,CAN总线还能在高噪声环境下稳定工作,对错误和冲突有很好的容错处理能力。 CAN总线的应用非常广泛,不仅限于汽车电子系统。它也被用于工业自动化、医疗设备、航空航天等领域。CAN总线通过提供实时通信、简化系统设计和减少线缆数量等优势,改善了系统的性能和可靠性。 学习CAN总线需要了解其基本原理、通信格式和应用场景。这里建议通过查阅相关资料,了解CAN总线的工作原理、数据帧格式、帧ID的使用和帧类型等基本概念。同时,学习CAN总线通信协议和错误处理机制也是很重要的。 为了加深理解,我们可以进行实际的实验和项目驱动。可以选择一个基于CAN总线的应用场景,例如汽车的ABS系统,通过搭建实验平台,了解CAN总线在ABS系统中的应用和通信过程。 总之,学习CAN总线需要掌握其基本原理和通信格式,并通过实践来加深理解。掌握CAN总线的知识可以为我们在汽车电子系统和其他领域的开发和应用提供帮助。 ### 回答2: CAN总线是一种常见的网络通信协议,用于在汽车和工业领域中传输数据。CAN总线具有高可靠性、稳定性和实时性,因此在现代车辆和工程中得到广泛应用。 CAN总线的工作原理是基于在网络上广播数据包的概念。每个节点(例如汽车中的传感器或控制器)通过总线连接到一个共享的通信线路上。当一个节点发送一个数据包时,其他节点都能够收到并解析其中的内容。这种分布式的通信方式使得不同节点能够高效地交换信息,从而实现复杂的系统控制。 在CAN总线基础教程中,通常会涵盖以下内容: 1. CAN总线的基本概念和特性:介绍CAN总线的工作原理、通信速率、数据格式等,帮助学习者了解CAN总线的基本知识。 2. CAN消息和帧格式:讲解CAN总线通信中的消息结构和帧格式,包括标准CAN帧和扩展CAN帧,帮助学习者理解数据的传输方式。 3. CAN标识符和过滤:介绍CAN总线中消息的标识符和过滤器的使用方法,帮助学习者实现数据的筛选和识别。 4. CAN物理层:讨论CAN总线的物理层接口和传输介质,如双绞线、光纤和无线等,帮助学习者选择适合的物理层配置。 5. CAN总线工具和调试:介绍CAN总线相关的硬件和软件工具,以及故障排除和调试技巧,帮助学习者实践和应用所学知识。 通过学习CAN总线基础教程,学习者可以获得在CAN总线应用和开发方面的基本知识和技能。CAN总线在汽车电子系统和工程领域的应用非常广泛,理解和掌握CAN总线的原理和操作将有助于学习者在相关领域中取得成功。 ### 回答3: CAN(Controller Area Network)总线是一种现场总线通信协议,广泛应用于汽车、工业控制和机械设备等领域。CAN总线的主要特点是高可靠性、高抗干扰能力和高实时性。 CAN总线由一组节点组成,节点之间通过CAN总线进行数据交换。节点包括CAN控制器和CAN收发器,控制器负责管理总线通信和数据传输,而收发器则负责将控制器产生的电信号转换为总线上的电压信号,以及将总线上的电压信号转换为控制器可读取的电信号。 CAN总线的数据传输采用分布式的方式,即任何一个节点都可以发送数据,其他节点则可以接收数据。节点之间的通信采用基于标识符的消息传递机制,每个消息会以一定的优先级发送,优先级高的消息会抢占总线资源。 CAN总线支持多种通信方式,包括点对点通信、广播通信和组播通信。点对点通信是指某一个节点向另一个指定节点发送消息;广播通信是指某一个节点向所有节点发送消息;组播通信是指某一个节点向一组指定节点发送消息。 CAN总线的应用非常广泛。在汽车领域,CAN总线被广泛应用于车载电子系统之间的通信,如发动机控制模块、防抱死制动系统和空调控制模块等。在工业控制领域,CAN总线常用于控制系统中各个设备之间的通信,如PLC(可编程逻辑控制器)和传感器之间的通信。 总之,CAN总线是一种高可靠性、高实时性的现场总线通信协议,广泛应用于汽车、工业控制和机械设备等领域,它提供了一种可靠和高效的数据传输方式,使得各个节点之间可以进行可靠的数据交换。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值