您的位置:首页 > 运维架构 > Linux

linux __setup

2015-11-15 23:50 615 查看
介绍的很详细,我也正好用了一次__setup(),蛮好用的:),今天回头看main.c,又搜索了一下,分享给大家:

 __setup(

//做了一次完整的盗贼,我毫无修改的抄袭了一次,但有时间会做量身裁减的//

from   http://www.linuxforum.net/forum/printthread.php?Cat=&Board=linuxK&main=470221&type=thread

关于__setup 在内核中的作用 

问题::

比如在printk.c有这样一句 

__setup("console=",console_setup); 

还有,在main.c中 

__setup("root=",root_dev_setup); 

人家的精彩回答:

你的这个问题,我从google上查找到了一些资料,再结合内核源代码,就在这里把这个问题说的清楚一点. 

首先,这里有一个简短的回答, 
http://mail.nl.linux.org/kernelnewbies/2003-03/msg00043.html 

从这上面的意思是这里会从main.c 中的checksetup函数中运行,这个函数是这样的 

static int __init checksetup(char *line) 



struct kernel_param *p; 

p = &__setup_start; 

do { 

int n = strlen(p->str); 

if (!strncmp(line,p->str,n)) { 

if (p->setup_func(line+n)) 

return 1; 



p++; 

} while (p < &__setup_end); 

return 0; 



这里的意思是从__setup_start开始处到__setup_end处中查找一个数据结构,这个数据结构中有str与setup_func这两个数据成员变量. 

只要与这里面的str与输入的参数字符串相匹配,就会调用个这个字符串后面所指的内容, 

对于你这里所说的 __setup("console=",console_setup); 就是你在启动linux内核的时候如果有这么一个参数输入console=ttyS1,那内核就会 

把默认的tty定位为ttyS1,这个在consol_setup函数的字符串处理中完成,因为它最后是确定prefered_console的参数. 

那把这在这里实现这个的内容是这样的, 

__setup() 是一个宏定义,在include/linux/init.h这个文件中. 

struct kernel_param { 

const char *str; 

int (*setup_func)(char *); 

}; 

extern struct kernel_param __setup_start, __setup_end; 

#define __setup(str, fn) \ 

static char __setup_str_##fn[] __initdata = str; \ 

static struct kernel_param __setup_##fn __attribute__((unused)) __initsetup = { __setup_str_##fn, fn } 

在这个情景中作了替换是这样的 

static char __setup_str_console_setup[] = "console="; 

static struct kernel_param __setup_console_setup = { __setup_str_console_setup, console_setup} 

这样你还可能不是很清楚,那你就要参考arch/i386/vmlinuz.lds这个关于ld 链接器的脚本文件有这样的一段 

__setup_start = .; 

.setup.init : { *(.setup.init) } 

__setup_end = .; 

这里的意思就是__setup_start是一个节的开始,而__setup_end是一个节的结束,这个节的名称是.setup,init, 

这个你可以用readelf -a这个来看一下你的vmlinux-2.4.20-8(后面的数字与你的内核版本有关)这个文件, 

可以看到有一个叫.setup.init的节,__setup_start就是指这个节的开始,那这个节中有什么内容呢,其实就是一个 

数据结构,一个就是str,一个就是setup_func,与我前面的说法相一致,那具体是什么呢,就是一个在.init.data节中存储的 

字符串-----__initdata是一个宏,就是(__attribute__ ((__section__ (".data.init")))), 所以你可以.data.init在vmlinux-2.4.20-8中的 

在文件中的偏移量与加载的的虚拟地址偏移量相减就可以得到, 

举个例子,所有的这些都是用readelf 与od 命令得到的 

我现在用的内核版本,它的.setup.init的节在0x26dd60的文件偏移处. 

[10] .data.init PROGBITS c0368040 268040 005d18 00 WA 0 0 32 

[11] .setup.init PROGBITS c036dd60 26dd60 0001b0 00 WA 0 0 4 

再查找console_setup在vmlinux-2.4.20-8所被映射为内存地址, 

840: c0355d40 343 FUNC LOCAL DEFAULT 9 console_setup 

这就可以知道了它所在的位置,就是0xc0355d40,这就是它的虚拟映射地址 

再用下面一条命令 

od --address-radix=x -t x4 vmlinux-2.4.20-8 |grep -A 20 26dd60 |head -20 | grep c0355d40 

可以得到 

26de40 c036943b c0355d10 c0369447 c0355d40 

很明显,这个函数的处理字符串在内存中的地址是0xc0369447,与前面得到的.data.init节在内存映射中的位置 

0xc0368040相减就是 0x1407,与.data.init在文件中的偏移量0x268040相加就得到0x269447 

这样用 

od --address-radix=x -a vmlinux-2.4.20-8 |grep -A 2 269440 

就可以得到下面的内容, 

269440 b l i n k = nul c o n s o l e = nul 

269450 r e s e r v e = nul nul nul nul nul nul nul nul 

269460 ` dc4 6 @ ` dc4 6 @ c p u f r e q = 

"console="这个值果真就在这里. 

(注:前面od 的选项 --address-radix= 表示的是显示文件偏移量的格式,默认下是o就是八进制, -t 表示显示文件二进制的形式

默认是o6 就是八进制的6位长,而-a表示显示的是字符串格式.) 

这是一点感受,与大家分享,希望大家提出宝贵意见. 

//////////////////////补充///////////////////////////

参见include/linux/init.h和vmlinux.lds 

1) 

所有标识为__init的函数在链接的时候都放在.init.text这个区段内, 

在这个区段中,函数的摆放顺序是和链接的顺序有关的,是不确定的。 

2) 

所有的__init函数在区段.initcall.init中还保存了一份函数指针, 

在初始化时内核会通过这些函数指针调用这些__init函数指针, 

并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等), 

注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关, 

和1)中所述的这些函数本身在.init.text区段中的顺序无关。 

在2.4内核中,这些函数指针的顺序也是和链接的顺序有关的,是不确定的。 

在2.6内核中,initcall.init区段又分成7个子区段,分别是 

.initcall1.init 

.initcall2.init 

.initcall3.init 

.initcall4.init 

.initcall5.init 

.initcall6.init 

.initcall7.init 

当需要把函数fn放到.initcall1.init区段时,只要声明 

core_initcall(fn); 

即可。 

其他的各个区段的定义方法分别是: 

core_initcall(fn) --->.initcall1.init 

postcore_initcall(fn) --->.initcall2.init 

arch_initcall(fn) --->.initcall3.init 

subsys_initcall(fn) --->.initcall4.init 

fs_initcall(fn) --->.initcall5.init 

device_initcall(fn) --->.initcall6.init 

late_initcall(fn) --->.initcall7.init 

而与2.4兼容的initcall(fn)则等价于device_initcall(fn)。 

各个子区段之间的顺序是确定的,即先调用.initcall1.init中的函数指针 

再调用.initcall2.init中的函数指针,等等。 

而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。 

在内核中,不同的init函数被放在不同的子区段中,因此也就决定了它们的调用顺序。 

这样也就解决了一些init函数之间必须保证一定的调用顺序的问题。 

3) 

这些顺序和make dep没有关系。 

(转自http://blog.csdn.net/jifengszf/archive/2009/04/22/4100193.aspx)

内核组件用__setup宏来注册关键字及相关联的处理函数,__setup宏在include/linux/init.h中定义,其原型如下:

__setup(string, function_handler)

其 中:string是关键字,function_handler是关联处理函数。__setup只是告诉内核在启动时输入串中含有string时,内核要去 执行function_handler。String必须以“=”符结束以使parse_args更方便解析。紧随“=”后的任何文本都会作为输入传给 function_handler。

下面的例子来自于net/core/dev.c,其中netdev_boot_setup作为处理程序被注册给“netdev=”关键字:

__setup("netdev=", netdev_boot_setup);

不 同的关键字可以注册相同的处理函数,例如在net/ethernet/eth.c中为“ether =”关键字注册了同样的处理函数 netdev_boot_setup。当代码作为模块被编译时,__setup宏被忽视,你可以在include/linux/init.h中看到 __setup宏是怎样变化的,不管后续包含它的文件是否是模块,include/linux/init.h都是独立的。

start_kernel两次调用parse_args解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用值能够兼顾到其中一类:

缺省选项:

绝大多数选项归于此类,这些选项由__setup宏定义并在第二次调用parse_args时处理。

先期(处理)选项:

在 内核启动阶段,有些选项要在其它选项之前被处理,内核提供了early_param宏以代替__setup宏申明此类选项。这些选项由 parse_early_params函数解析。early_param宏和__setup宏仅有的不同就是前者设置了一个特殊标志让内核能够区分两种不 同的状况。这个标志是我们将在“.init.setup内存区”小节中看到的obs_kernel_param结构的一部分。

启动时选项在内核 2.6中的处理方式已经改变,但并非所有的内核代码都因此而更新。在最近一次改变之前,还仅用__setup宏。因此,遗留下来将被更新的代码现在使用 __obsolete_setup宏。但用户用__obsolete_setup宏定义的选项给内核时,内核打印一条警告消息说明它已是废弃状态,并提供 一个文件指针和随后被公告的源代码行信息。

图7-1概述了几个宏之间的关系:它们都包裹了普通的__setup_param函数。

内核组件用__setup宏来注册关键字及相关联的处理函数,__setup宏在include/linux/init.h中定义,其原型如下:

#define __setup(str, fn) __setup_param(str, fn, fn, 0) (__setup_param定义请参见include/linux/init.h) 其中:str是关键字,fn是关联处理函数。__setup只是告诉内核在启动时输入串中含有str时,内核要去执行fn。Str必须以“=”符结束以使parse_args更方便解析。紧随“=”后的任何文本都会作为输入传给
fn。

下面的例子来自于net/core/dev.c,其中netdev_boot_setup作为处理程序被注册给“netdev=”关键字:

__setup("netdev=", netdev_boot_setup);

在这个情景中作了替换是这样的

static char __setup_str_netdev_setup[] = "netdev=";

static struct kernel_param __setup_netdev_setup =

{ __setup_str_netdev_setup, console_setup}

不同的关键字可以注册相同的处理函数,例如在net/ethernet/eth.c中为“ether=”关键字注册了同样的处理函数 netdev_boot_setup。当代码作为模块被编译时,__setup宏被忽视,你可以在include/linux/init.h中看到 __setup宏是怎样变化的,不管后续包含它的文件是否是模块,include/linux/init.h都是独立的。

start_kernel两次调用parse_args解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用值能够兼顾到其中一类:

缺省选项:绝大多数选项归于此类,这些选项由__setup宏定义并在第二次调用parse_args时处理。

先期(处理)选项:

#define early_param(str, fn) __setup_param(str, fn, fn, 1) 在 内核启动阶段,有些选项要在其它选项之前被处理,内核提供了early_param宏以代替__setup宏申明此类选项。这些选项由 parse_early_params函数解析。early_param宏和__setup宏仅有的不同就是前者设置了一个特殊标志让内核能够区分两种不同的状况。

启动时选项在内核 2.6中的处理方式已经改变,但并非所有的内核代码都因此而更新。在最近一次改变之前,还仅用__setup宏。因此,遗留下来将被更新的代码现在使用 __obsolete_setup宏。但用户用__obsolete_setup宏定义的选项给内核时,内核打印一条警告消息说明它已是废弃状态,并提供一个文件指针和随后被公告的源代码行信息。

 

图7-1概述了几个宏之间的关系:它们都包裹了普通的__setup_param函数。

 

图7-1 setup_param宏及其包裹物

注意:传给__setup宏的程序被放到.init.setup内存节,这样,在“启动时初始化代码”一节中可以很清晰的看出这样做的效果。

在arch/arm/kernel/vmlinux.lds.S关于ld 链接器的脚本文件有这样的一段 __setup_start = .;

.setup.init : { *(.setup.init) }

__setup_end = .;

有如下的理解:

1、 所有标识为__init的函数在链接的时候都放在.init.text这个区段内, 在这个区段中,函数的摆放顺序是和链接的顺序有关的,是不确定的。

2、所有的__init函数在区段.initcall.init中还保存了一份函数指针, 在初始化时内核会通过这些函数指针调用这些__init函数指针, 并在整个初始化完成后,释放整个init区段(包括.init.text,.initcall.init等), 注意,这些函数在内核初始化过程中的调用顺序只和这里的函数指针的顺序有关,

和1)中所述的这些函数本身在.init.text区段中的顺序无关。

在2.4内核中,这些函数指针的顺序也是和链接的顺序有关的,是不确定的。

在2.6内核中,initcall.init区段又分成7个子区段,分别是

.initcall1.init

.initcall2.init

.initcall3.init

.initcall4.init

.initcall5.init

.initcall6.init

.initcall7.init

当需要把函数fn放到.initcall1.init区段时,只要声明 core_initcall(fn); 即可。

其他的各个区段的定义方法分别是:

core_initcall(fn)--->.initcall1.init

postcore_initcall(fn)--->.initcall2.init

arch_initcall(fn)--->.initcall3.init

subsys_initcall(fn)--->.initcall4.init

fs_initcall(fn)--->.initcall5.init

device_initcall(fn)--->.initcall6.init

late_initcall(fn)--->.initcall7.init

而与2.4兼容的initcall(fn)则等价于device_initcall(fn)。

各个子区段之间的顺序是确定的,即先调用.initcall1.init中的函数指针,再调用.initcall2.init中的函数指针,等等。 而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。 在内核中,不同的init函数被放在不同的子区段中,因此也就决定了它们的调用顺序。 这样也就解决了一些init函数之间必须保证一定的调用顺序的问题。

注册内核选项:模块参数使用module_param系列的宏注册,内核选项则使用__setup宏来注册。

__setup宏在include/linux/init.h文件中定义。

171 #define __setup(str, fn)     \

172   __setup_param(str, fn, fn, 0)

__setup需要两个参数,其中str是内核选项的名字,fn是该内核选项关联的处理函数。__setup宏告诉内核,在启动时如果检测到内核选项str,则执行函数fn。str除了包括内核选项名字之外,必须以“=”字符结束。

不同的内核选项可以关联相同的处理函数,比如内核选项netdev和ether都关联了netdev_boot_setup函数。

除了__setup宏之外,还可以使用early_param宏注册内核选项。它们的使用方式相同,不同的是,early_param宏注册的内核选项必须要在其他内核选项之前被处理。

两次解析

相应于__setup宏和early_param宏两种注册形式,内核在初始化时,调用了两次parse_args函数进行解析。

parse_early_param();

parse_args("Booting kernel", static_command_line, __start___param,

    __stop___param - __start___param,

           &unknown_bootoption);

parse_args的第一次调用就在parse_early_param函数里面,为什么会出现两次调用parse_args的情况?这是因为内核选项又分成了两种,就像现实世界中的我们,一种是普普通通的,一种是有特权的,有特权的需要在普通选项之前进行处理。

内核选项相对来说就要单纯得多,,直接使用early_param宏去声明,让你一眼就看出它是有特权的。使用early_param声明的那些选项就会首先由parse_early_param去解析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux 内核