深入理解Linux网络技术内幕 第7章 组件初始化的内核基础架构

组件初始化的内核基础架构

  • 引导期间的内核选项
    • 注册关键字
    • 两遍分析
    • 使用引导选项配置网络设备
  • 模块初始化代码
    • 新模型:宏卷标
  • 引导期间初始化函数
    • xxx_initcall宏
    • __initcall和__exitcall和模块
  • 内存最优化
    • __init宏和__exit宏
    • 动态宏定义

引导期间的内核选项

Linux允许用户把内核配置选项传递给引导程序,可以使用此机制在引导期间调整内核。
parse_args函数用于解析输入字符串,输入字符串是形如 key=value形式的参数。寻找到特定关键字后,启用适当的处理函数。

注册关键字

内核组件使用__setup宏,注册关键字和处理函数。如网络子系统注册的关键字

__setup("netdev=", netdev_boot_setup);

同一个处理函数可以和多个关键字关联。比如函数netdev_boot_setup又被关键字ether=关联。

__setup("ether=", netdev_boot_setup);

当一段代码被编译成模块时,__setup宏被定义成空操作。
start_kernel两次调用parse_args解析引导配置信息的原因是,引导选项分成两类,每次调用都处理其中的一类。

  • 默认选项
    由__setup宏进行定义,这个宏定义的选项在第二次调用parse_args时处理。
  • 初期选项
    由early_param宏定义,这个宏定义的选项在第一次调用parse_early_param函数时处理。

根据上面的描述可以得出结论使用early_param函数定义的初期选项比使用__setup定义的默认选项更早执行。
传递给__setup的输入函数会被放到.init.setup内存区。

#ifndef MODULE
struct obs_kernel_param {
     
	const char *str;
	int (*setup_func)(char *);
	int early;
};

/*
 * Only for really core code.  See moduleparam.h for the normal way.
 *
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.setup.
 */
#define __setup_param(str, unique_id, fn, early)			\
	static const char __setup_str_##unique_id[] __initconst		\
		__aligned(1) = str; 					\
	static struct obs_kernel_param __setup_##unique_id		\
		__used __section(.init.setup)				\
		__attribute__((aligned((sizeof(long)))))		\
		= { __setup_str_##unique_id, fn, early }

#define __setup(str, fn)						\
	__setup_param(str, fn, fn, 0)
/*
 * NOTE: fn is as per module_param, not __setup!
 * Emits warning if fn returns non-zero.
 */
#define early_param(str, fn)						\
	__setup_param(str, fn, fn, 1)
#else /* MODULE */
#define __setup_param(str, unique_id, fn)	/* nothing */
#define __setup(str, func) 			/* nothing */
#endif
	

两遍分析

第一遍只处理设置了early标记的选项。第二遍处理剩余的选项。

extern const struct obs_kernel_param __setup_start[], __setup_end[];

分析上一小节的代码可以看出当我们定义一个选项时,会定义一个struct obs_kernel_param结构。所有选项数据会放在同一个内存段中(.init.setup区),内核启动时使用__setup_start和__setup_end两个指针指向那个内存段的开始结束。
这样做的原因是:

  • 容易遍历所有实例。
  • 这些数据不再需要时,可以快速释放。

使用引导选项配置网络设备

前边已经知道使用"ether="和"netdev="两个关键字注册了函数netdev_boot_setup。
netdev_boot_setup函数将参数解析后放入dev_boot_setup数组中。
dev_boot_setup是一个struct netdev_boot_setup数组。
struct netdev_boot_setup结构中有一个struct ifmap成员,用于存储输入配置的值。

 *	Device mapping structure. I'd just gone off and designed a 
 *	beautiful scheme using only loadable modules with arguments
 *	for driver options and along come the PCMCIA people 8)
 *
 *	Ah well. The get() side of this is good for WDSETUP, and it'll
 *	be handy for debugging things. The set side is fine for now and
 *	being very small might be worth keeping for clean configuration.
 */

/* for compatibility with glibc net/if.h */
#if __UAPI_DEF_IF_IFMAP
struct ifmap {
     
	unsigned long mem_start;
	unsigned long mem_end;
	unsigned short base_addr; 
	unsigned char irq;
	unsigned char dma;
	unsigned char port;
	/* 3 bytes spare */
};
#endif /* __UAPI_DEF_IF_IFMAP */

/*
 * This structure holds boot-time configured netdevice settings. They
 * are then used in the device probing.
 */
struct netdev_boot_setup {
     
	char name[IFNAMSIZ];
	struct ifmap map;
};
static int netdev_boot_setup_add(char *name, struct ifmap *map)
{
     
	struct netdev_boot_setup *s;
	int i;

	s = dev_boot_setup;
	for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
     
		if (s[i].name[0] == '\0' || s[i].name[0] == ' ') {
     
			memset(s[i].name, 0, sizeof(s[i].name));
			strlcpy(s[i].name, name, IFNAMSIZ);
			memcpy(&s[i].map, map, sizeof(s[i].map));
			break;
		}
	}

	return i >= NETDEV_BOOT_SETUP_MAX ? 0 : 1;
}

/*
 * Saves at boot time configured settings for any netdevice.
 */
int __init netdev_boot_setup(char *str)
{
     
	int ints[5];
	struct ifmap map;

	str = get_options(str, ARRAY_SIZE(ints), ints);
	if (!str || !*str)
		return 0;

	/* Save settings */
	memset(&map, 0, sizeof(map));
	if (ints[0] > 0)
		map.irq = ints[1];
	if (ints[0] > 1)
		map.base_addr = ints[2];
	if (ints[0] > 2)
		map.mem_start = ints[3];
	if (ints[0] > 3)
		map.mem_end = ints[4];

	/* Add new entry to the list */
	return netdev_boot_setup_add(str, &map);
}

__setup("netdev=", netdev_boot_setup);

引导结束时,网络代码可以使用netdev_boot_setup_check函数根据设备名称,检查给定接口是否在引导期间传入配置信息。

/**
 * netdev_boot_setup_check	- check boot time settings
 * @dev: the netdevice
 *  * Check boot time settings for the device.
 * The found settings are set for the device to be used
 * later in the device probing.
 * Returns 0 if no settings found, 1 if they are.
 */
int netdev_boot_setup_check(struct net_device *dev)
{
     
	struct netdev_boot_setup *s = dev_boot_setup;
	int i;

	for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
     
		if (s[i].name[0] != '\0' && s[i].name[0] != ' ' &&
		    !strcmp(dev->name, s[i].name)) {
     
			dev->irq = s[i].map.irq;
			dev->base_addr = s[i].map.base_addr;
			dev->mem_start = s[i].map.mem_start;
			dev->mem_end = s[i].map.mem_end;
			return 1;
		}
	}
	return 0;
}

模块初始化代码

新模型:宏卷标

为了使内核具有更高的可读性,引入一组宏,比如__init、__exit等
这些宏允许内核决定要把每个模块的那些代码引入到内核镜像中。
同时这些宏至少提供下列两项服务:

  • 定义模块加载时的函数,无论这些模块组件是静态链接还是以模块的形式加载。
  • 定义初始化函数之间的次序,强制组件之间遵循相互依赖性。

Linux内核使用各种不同的宏为函数和数据结构标记特殊属性。有些宏辉通知链接器把带有共同属性的代码或数据结构放到特定的内存区,这样内核可以轻易管理所有具有共同属性的对象。

函数所用的宏:

  • __init宏
    引导期间初始化函数,针对那些引导结束后不在需要使用的函数。

  • __exit宏
    __init的配对宏,当相关的内核组件被关闭的时候调用。通常用于修饰module_exit函数。

  • core_initcall

  • postcore_initcall

  • arch_initcall

  • subsys_initcall

  • fs_initcall

  • device_initcall

  • last_initcall
    以上这些宏标记那些必须在引导期间执行的初始化函数。

初始化数据结构所用的宏:

  • __initdata
    用于引导期间已经初始化的数据结构。
  • __exitdata
    用于标记__exitcall函数所用的数据结构。如果__exitcall标记的函数不再使用,则__exitdata标记的数据也不再使用。

设备初始化函数使用的初始化宏:

  • __devinit
    用于标记初始化设备的函数。
  • __devexit
    用于标记设备删除时被调用的函数。
  • __devexit_p
    标记初始化为__devexit函数的指针
  • __devinitdata
    标记被标记位__devinit函数使用的数据结构。
  • __devexit_data
    标记被标记位__devexit函数使用的数据结构。

引导期间初始化函数

xxx_initcall宏

内核引导早期阶段有两个工种

  1. 各个关键子系统的初始化顺序有顺序要求。比如PCI层的初始化之前不可以初始化使用PCI的设备。
  2. 其他不需要严格执行次序的内核组件具有相同的优先级,可以按照任何次序执行。

前一小节列出了很多以_initcall结尾的宏,这些宏分别将各自的函数放在各自的内存段中,由do_initcalls函数按照段的优先级分别调用。
do_initcall_level函数将一个段中的所有函数都取出来依次调用。

static void __init do_initcall_level(int level)
{
     
	initcall_entry_t *fn;

	strcpy(initcall_command_line, saved_command_line);
	parse_args(initcall_level_names[level],
		   initcall_command_line, __start___param,
		   __stop___param - __start___param,
		   level, level,
		   NULL, &repair_env_string);

	trace_initcall_level(initcall_level_names[level]);
	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
     
	int level;

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

__initcall和__exitcall和模块

module_init和module_exit宏分别标记那些在模块初始化和卸载时执行的函数。
从下边的代码中可以看出当该模块被内核静态编译(不是编译成模块时)被定义为__initcall

#ifndef MODULE
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 *
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)	__initcall(x);

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 *
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)	__exitcall(x);

#else /* MODULE */

跟踪module_init宏,将其相关的宏整理如下,可以发现module_init修饰的函数被放入了.initcall6的内存段中。

#define device_initcall(fn)		__define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x)	__initcall(x);

在第五章的设备处理初始化一章介绍的net_dev_init函数,放到了.initcall4中,因此就可以保证net_dev_init函数肯定会先于驱动程序的初始化函数运行。

#define subsys_initcall(fn)		__define_initcall(fn, 4)
subsys_initcall(net_dev_init);

内存最优化

内核代码和数据会永久驻留在内存中,所以应该减少内存浪费。初始化代码大部分只会执行一次,因此在使用完成之后可以被释放。

  • 加载模块时,module_init只会执行一次。当模块和内核静态链接时,引导期间,module_init修饰的函数运行后可以被释放。
  • 模块与内核静态链接时,module_exit不会执行,不会将其放入内核镜像中。

__init宏和__exit宏

内核早期阶段执行的函数都会标记__init宏。大多数的module_init修饰函数也标记位这个宏。
深入理解Linux网络技术内幕 第7章 组件初始化的内核基础架构_第1张图片
查看__init的定义可以发现被__init修饰过的函数被放入了.init.text这个section,这个section会由kernel_init函数调用free_initmem进行释放。

#define __init		__section(.init.text) __cold  __latent_entropy __noinitretpoline

__exit宏用于将函数放到.exit.text这个section。这个section在链接内核时直接丢掉。

#define __exit          __section(.exit.text) __exitused __cold notrace

xxx__initcall宏
free_initmem也会将xxx__initcall各个段的内存丢弃掉。

__exitcall宏
__exitcall修饰的函数放到.text.exit的section链接期间被丢弃。

__devinit宏当内核没有编译为支持热插拔时,引导阶段结束后就不需要__devinit的函数,因此在不支持热插拔时__devinit定义成__init。
__devexit宏当内核没有编译为支持热插拔时,__devexit的函数不会被调用可以被丢弃掉。

动态宏定义

  • CONFIG_MODULEd
    当内核支持可加载模块时定义。

  • CONFIG_HOTPLUG
    当内核支持热插拔时定义

  • MODULE
    当改建所属的内核组件编译为模块时定义。

你可能感兴趣的:(深入理解LINUX网络技术内幕,内核,linux)