解析Linux内核模块的链接顺序

来源:互联网 发布:淘宝店链接地址 编辑:程序博客网 时间:2024/06/05 05:48
关键字:Linux

  昨天晚上把我们设备的内核升级到最新的Linux-3.3上去了,这个版本主要新特性包括:合并了来自Android项目的内核代码,支持新的架构TI C6X,改进了Btrfs文件系统的balance操作、新的除错工具和RAID restripping;新的虚拟网络交换机Open vSwitch;支持EFI引导固件;改进内存管理等,这里我们比较感兴趣的是:更快更具弹性的网络设备接口teaming;通过字节序列限制改进网络延迟;网络优先控制组允许管理员动态设置网络流量的优先次序。

  在移植好linux-3.3并跑起来后发现,以前(内核<=linux-3.0)设备上的两张网卡DM9000和DM9161分别对应的网络设备是eth0和eth1,由于在linux-3.3中内核调整了drivers/net/ethernet的目录结构,结果编译后运行新的内核发现,设备上的DM9000对应到eth1上,而DM9161对应到了eth0上。这与以前的网卡的对应关系不一致,所以希望调整这两个模块的链接顺序保证与以前的兼容。

  首先,我们可以查看Linux内核编译完成后的System.map文件,在这个文件中我们可以看到macb(dm9161驱动模块)链接到了dm9000驱动之前,如下所示:

  c03b6d40 t __initcall_tun_init6

  c03b6d44 t __initcall_macb_init6

  c03b6d48 t __initcall_dm9000_init6

  c03b6d4c t __initcall_ppp_init6

  c03b6d50 t __initcall_ppp_async_init6

  我尝试修改arch/arm/mach-at91/board-sam9260ek.c中DM9000和DM916设备添加的顺序,即先添加 dm9000,后添加dm9161。编译后运行发现,结果还是一样。自己想了想,这也在情理之中。因为这个出现这个问题的主要原因是这两个驱动加载的先后顺序,而不是设备添加的先后顺序。

  在Linux内核中维护着两个链,一个设备链,一个驱动链,他们两个就像情侣一样互相依赖,互相纠缠在一起的。当我们新添加一个设备时,他会被加入到设备链上,这时内核这个红娘会就会到驱动链上给他找他的另外一半(驱动),看是否有哪个驱动看上了他(这个驱动是否支持这个设备),如果找到了这个驱动,那么设备就能够使用(大家纠缠到一块了,该干嘛就干嘛去了)。而如果没有找到,那么设备就只能默默地在那里等待他的另一半的出现。下面是arch/arm/mach-at91/board-sam9260ek.c添加设备的代码:

  static void __init ek_board_init(void){ /* Serial */

  at91_add_device_serial(); /* USB Host */

  at91_add_device_usbh(&ek_usbh_data); /* USB Device */

  at91_add_device_udc(&ek_udc_data); /* SPI */

  at91_add_device_spi(ek_spi_devices, ARRAY_SIZE(ek_spi_devices)); /* NAND */

  ek_add_device_nand(); /* Ethernet */ ek_add_device_dm9000(); /* Add dm9000 driver by guowenxue, 2012.04.11 */

  at91_add_device_eth(&ek_macb_data); /* MMC */

  at91_add_device_mmc(0, &ek_mmc_data); /* I2C */

  at91_add_device_i2c(ek_i2c_devices, ARRAY_SIZE(ek_i2c_devices)); /* SSC (to AT73C213) */

  #if defined(CONFIG_SND_AT73C213) || defined(CONFIG_SND_AT73C213_MODULE)

  at73c213_set_clk(&at73c213_data); /* Modify by guowenxue, 2012.04.11 */

  #endif

  at91_add_device_ssc(AT91SAM9260_ID_SSC, ATMEL_SSC_TX);

  #if 0 /* comment by guowenxue */ /* LEDs */

  at91_gpio_leds(ek_leds, ARRAY_SIZE(ek_leds)); /* Push Buttons */

  ek_add_device_buttons();

  #endif

  }

  MACHINE_START(AT91SAM9260EK, "Atmel AT91SAM9260-EK") /* Maintainer: Atmel */

  .timer = &at91sam926x_timer,

  .map_io = at91_map_io,

  .init_early = ek_init_early,

  .init_irq = at91_init_irq_default,

  .init_machine = ek_board_init,MACHINE_END

  MACHINE_START主要是定义了"struct machine_desc"的类型,放在 section(".arch.info.init"),是初始化数据,Kernel 起来之后将被丢弃。

  其余各个成员函数在setup_arch()中被赋值到内核结构体,在不同时期被调用:

  1. .init_machine 在 arch/arm/kernel/setup.c 中被 customize_machine 调用,放在 arch_initcall() 段里面,会自动按顺序被调用。

  2. .init_irq在start_kernel() --> init_IRQ() --> init_arch_irq()中被调用

  3. .map_io 在 setup_arch() --> paging_init() --> devicemaps_init()中被调用

  4. .timer是定义系统时钟,定义TIMER4为系统时钟,在arch/arm/mach-at91/at91sam926x_time.c中实现。在start_kernel() --> time_init()中被调用。

  5. .boot_params是bootloader向内核传递的参数的位置,这要和bootloader中参数的定义要一致。

  其他主要都在 setup_arch() 中用到。

  当在Linux内核启动调用ek_board_init()时,就会调用ek_add_device_dm9000()

  和at91_add_device_eth(&ek_macb_data)来分别将dm9161和dm9000这两个设备添加到设备链上去。然后,他们就开始在链表上苦苦等待他的另一半(相应驱动)的出现。

  这里我们只是调整这两个网络设备在设备链上的位置,但问题的本质是驱动链接的位置是dm9161在前,dm9000在后,这样dm9161驱动先加载后就找到设备dm9161,这样他使用了eth0这个设备;而dm9000的驱动后加载,这样他对应的设备名就是eth1了。这里来分析为什么是先加载 dm9161,后加载dm9000这个驱动,只有了解了这个原因,我们才能调整他们的加载顺序。

  几乎每个linux驱动都会调用module_init(它和module_exit一起定义在Init.h (/include/linux) 中。没错,驱动的加载就靠它。为什么需要这样一个宏?原因是按照一般的编程想法,各部分的初始化函数会在一个固定的函数里调用比如:

  void init(void)

  {

  init_a();

  init_b();

  }

  如果再加入一个初始化函数呢,那么在init_b()后面再加一行init_c();这样确实能完成我们的功能,但这样有一定的问题,就是不能独立的添加初始化函数,每次添加一个新的函数都要修改init函数。可以采用另一种方式来处理这个问题,只要用一个宏来修饰一下:

  void init_a(void)

  {

  }

  __initlist(init_a, 1);

  它是怎么样通过这个宏来实现初始化函数列表的呢?先来看__initlist的定义:

  #define __init __attribute__((unused, __section__(".initlist")))

  #define __initlist(fn, lvl) /

  static initlist_t __init_##fn __init = { /

  magic: INIT_MAGIC, /

  callback: fn, /

  level: lvl }

  请注意:__section__(".initlist"),这个属性起什么作用呢?它告诉连接器这个变量存放在。initlist区段,这是 GNU/GCC的特性,关于这部分内容大家可以参考GNU连接器的说明文档。如果所有的初始化函数都是用这个宏,那么每个函数会有对应的一个 initlist_t结构体变量存放在。initlist区段,也就是说我们可以在。initlist区段找到所有初始化函数的指针。怎么找到。initlist区段的地址呢?

  extern u32 __initlist_start;

  extern u32 __initlist_end;

  这两个变量起作用了,__initlist_start是。initlist区段的开始,__initlist_end是结束,通过这两个变量我们就可以访问到所有的初始化函数了。这两个变量在那定义的呢?在一个连接器脚本文件里(别告诉我说你不知道连接器脚本是啥,如果不知道,好好恶补一下)。

  . = ALIGN(4); .initlist : { __initlist_start = .; *(。initlist) __initlist_end = .; }

  这两个变量的值正好定义在。initlist区段的开始和结束地址,所以我们能通过这两个变量访问到所有的初始化函数。

  与此类似,内核中也是用到这种方法,所以我们写驱动的时候比较独立,不用我们自己添加代码在一个固定的地方来调用我们自己的初始化函数和退出函数,连接器已经为我们做好了。先来分析一下module_init。他在include/linux/init.h文件中定义如下:

  #define module_init(x) __initcall(x);#define __initcall(fn) device_initcall(fn)

  #define pure_initcall(fn) __define_initcall("0",fn,0)

  #define core_initcall(fn) __define_initcall("1",fn,1)

  #define core_initcall_sync(fn) __define_initcall("1s",fn,1s)

  #define postcore_initcall(fn) __define_initcall("2",fn,2)

  #define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)

  #define arch_initcall(fn) __define_initcall("3",fn,3)

  #define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)

  #define subsys_initcall(fn) _define_initcall("4",fn,4)

  #define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)

  #define fs_initcall(fn) __define_initcall("5",fn,5)

  #define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)

  #define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)

  #define device_initcall(fn) __define_initcall("6",fn,6)

  #define device_initcall_sync(fn) __define_initcall("6s",fn,6s)

  #define late_initcall(fn) __define_initcall("7",fn,7)

  #define late_initcall_sync(fn) __define_initcall("7s",fn,7s)

  #define __define_initcall(level,fn,id) \

  static initcall_t __initcall_##fn##id __used \

  __attribute__((__section__(".initcall" level ".init"))) = fn

  如果某驱动想以func作为该驱动的入口,则可以如下声明:module_init(func);被上面的宏处理过后,变成 __initcall_func6 __used加入到内核映像的".initcall"区(这就是我们上面System.map文件中__initcall_macb_init6和 __initcall_dm9000_init6的来历)。内核的加载的时候,会搜索".initcall"中的所有条目,并按优先级加载它们,普通驱动程序的优先级是6。其它模块优先级列出如下:值越小,越先加载。从上可以看到,被声明为pure_initcall的最先加载。

  module_init除了初始化加载之外,还有后期释放内存的作用。linux kernel中有很大一部分代码是设备驱动代码,这些驱动代码都有初始化和反初始化函数,这些代码一般都只执行一次,为了有更有效的利用内存,这些代码所占用的内存可以释放出来。

  linux 就是这样做的,对只需要初始化运行一次的函数都加上__init属性,__init 宏告诉编译器如果这个模块被编译到内核则把这个函数放到(。init.text)段,module_exit的参数卸载时同__init类似,如果驱动被编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,显然__init和__exit对动态加载的模块是无效的,只支持完全编译进内核。

  在kernel初始化后期,释放所有这些函数代码所占的内存空间。连接器把带__init属性的函数放在同一个section里,在用完以后,把整个section释放掉。当函数初始化完成后这个区域可以被清除掉以节约系统内存。Kenrel启动时看到的消息“Freeing unused kernel memory: xxxk freed”同它有关。

  也就是在写驱动的时候,通过module_init()宏,告诉我们的驱动函数入口放到。initcall节中的哪个部分,那么Linux内核在启动的时候又是怎么调用我们的这些驱动入口函数的呢?

  Linux 系统使用两种方式去加载系统中的模块:动态和静态。这里我们dm9161和dm9000的驱动是以静态的方式程序编译到Linux内核中,Linux系统启动时会进入C函数入口(下面函数都在init/main.c文件中)start_kernel()->rest_init()->kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND)->kernel_init()->do_basic_setup()->do_initcalls()。

  下面是do_initcalls()的定义:

  extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[];

  static void __init do_initcalls(void){

  initcall_t *fn;

  for (fn = __early_initcall_end; fn < __initcall_end; fn++)

  do_one_initcall(*fn);

  }

  do_initcalls 函数中会将在__early_initcall_end 和__initcall_end之间定义的各个模块依次加载。那么在__early_initcall_end 和 __initcall_end之间都有些什么呢?我们可以查看arch/arm/kernel/vmlinux.lds文件中关于。initcall.init段:

  __initcall_start = .; *(。initcallearly.init) __early_initcall_end = .; *(。initcall0.init) *(。initcall0s.init) *(。initcall1.init) *

  (。initcall1s.init) *(。initcall2.init) *(。initcall2s.init) *(。initcall3.init) *(。initcall3s.init) *(。initcall4.init) *(。initcall4s.

  init) *(。initcall5.init) *(。initcall5s.init) *(。initcallrootfs.init) *(。initcall6.init) *(。initcall6s.init) *(。initcall7.init) *(。

  initcall7s.init) __initcall_end = .;

  可以看出在这两个宏之间依次排列了14个等级的宏,由于这其中的宏是按先后顺序链接的,所以也就表示,这14个宏有优先级:0>0s>1>1s>2>2s……>7>7s,这里的优先级也就意味着谁的优先级高,那么谁就会被先加载。关于这宏有什么具体的意义呢,这就要看我们之前提到的include/linux/init.h文件中的定义了:

  #define module_init(x) __initcall(x);#define __initcall(fn) device_initcall(fn)

  #define pure_initcall(fn) __define_initcall("0",fn,0)

  #define core_initcall(fn) __define_initcall("1",fn,1)

  #define core_initcall_sync(fn) __define_initcall("1s",fn,1s)

  #define postcore_initcall(fn) __define_initcall("2",fn,2)

  #define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)

  #define arch_initcall(fn) __define_initcall("3",fn,3)

  #define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)

  #define subsys_initcall(fn) __define_initcall("4",fn,4)

  #define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)

  #define fs_initcall(fn) __define_initcall("5",fn,5)

  #define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)

  #define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)

  #define device_initcall(fn) __define_initcall("6",fn,6)

  #define device_initcall_sync(fn) __define_initcall("6s",fn,6s)

  #define late_initcall(fn) __define_initcall("7",fn,7)

  #define late_initcall_sync(fn) __define_initcall("7s",fn,7s)

  从上面分析,我们可以看到,我们的DM9000和DM9161都是使用module_init()来定义的,那么他们最终都是在同一个级别( __define_initcall("6",fn,6))中加载。对于这些函数指针的顺序也是和链接的顺序有关的,但具体是不确定的(不通目录下的链接顺序),但我通过修改Makefile中的编译顺序,把DM9000的编译放在DM9161之前就OK了。这样可以看出,对于同一目录下的驱动文件,我们可以通过调整他们在Makefile中编译的顺序来解决这个问题:

  [guowenxue@centos6 linux-3.3]$ vim drivers/net/ethernet/Makefile

  obj-$(CONFIG_DM9000) += davicom/

  obj-$(CONFIG_NET_CADENCE) += cadence/

  编译后再看System.map文件:

  c03b6d40 t __initcall_tun_init6

  c03b6d44 t __initcall_dm9000_init6

  c03b6d48 t __initcall_macb_init6

  c03b6d4c t __initcall_ppp_init6

  c03b6d50 t __initcall_ppp_async_init

  系统启动打印:

  ……

  bonding: Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)

  tun: Universal TUN/TAP device driver, 1.6

  tun: (C) 1999-2004 Max Krasnyansky

  dm9000 Ethernet Driver, V1.31

  eth0: dm9000a at c4896000,c489e044 IRQ 111 MAC: 00:30:c2:12:03:19 (chip)

  macb macb: (unregistered net_device): invalid hw address, using random

  MACB_mii_bus: probed

  macb macb: eth1: Cadence MACB at 0xfffc4000 irq 21 (6e:cf:99:c4:e4:5b)

  macb macb: eth1: attached PHY driver [Generic PHY] (mii_bus:phy_addr=macb-ffffffff:00, irq=-1)

  PPP generic driver version 2.4.2

  PPP BSD Compression module registered

  PPP Deflate Compression module registered

  PPP MPPE Compression module registered

  NET: Registered protocol family 24

  usbcore: registered new interface driver rt2800usb

  ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver

  at91_ohci at91_ohci: AT91 OHCI

  at91_ohci at91_ohci: new USB bus registered, assigned bus number 1

  at91_ohci at91_ohci: irq 20, io mem 0x00500000

  hub 1-0:1.0: USB hub found

  hub 1-0:1.0: 2 ports detected

  Initializing USB Mass Storage driver…

  ……

  如果这种方法不能解决的话,那么我们可以修改dm9161的驱动,将module_init宏改成device_initcall_sync,这样加载的级别降低后也能改变他们的加载顺序,如下。

  [guowenxue@centos6 linux-3.3]$ vim drivers/net/ethernet/cadence/macb.c

  …

  //module_init(macb_init);

  device_initcall_sync(macb_init);

  module_exit(macb_exit);

  这样编译后的System.map文件显示:

  c03b6d40 t __initcall_tun_init6

  c03b6d44 t __initcall_dm9000_init6

  c03b6d48 t __initcall_ppp_init6

  c03b6d4c t __initcall_ppp_async_init6

  c03b6d50 t __initcall_bsdcomp_init6

  …

  c03b6f6c t __initcall_macb_init6s

  c03b6f70 t __initcall_at91_clock_reset7

  c03b6f74 t __initcall_init_oops_id7

  c03b6f78 t __initcall_printk_late_init7

  c03b6f7c t __initcall_sched_init_debug7

  c03b6f80 t __initcall_ubifs_init7

  这时系统启动的过程:

  …

  bonding: Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)

  tun: Universal TUN/TAP device driver, 1.6

  tun: (C) 1999-2004 Max Krasnyansky

  dm9000 Ethernet Driver, V1.31

  eth0: dm9000a at c4896000,c489e044 IRQ 111 MAC: 00:30:c2:12:03:19 (chip)

  PPP generic driver version 2.4.2

  PPP BSD Compression module registered

  PPP Deflate Compression module registered

  PPP MPPE Compression module registered

  NET: Registered protocol family 24

  ……

  Bridge firewalling registered

  lib80211: common routines for IEEE802.11 drivers

  macb macb: (unregistered net_device): invalid hw address, using random

  MACB_mii_bus: probed

  macb macb: eth1: Cadence MACB at 0xfffc4000 irq 21 (b6:6c:eb:6a:dc:cc)

  macb macb: eth1: attached PHY driver [Generic PHY] (mii_bus:phy_addr=macb-ffffffff:00, irq=-1)

  rtc-ds1307 0-0068: setting system clock to 2000-01-01 00:00:00 UTC (946684800)

  RAMDISK: gzip image found at block 0

  EXT2-fs (ram0): warning: mounting unchecked fs, running e2fsck is recommended

  VFS: Mounted root (ext2 filesystem) on device 1:0.

  Freeing init memory: 140K

原创粉丝点击