Chinaunix首页 | 论坛 | 博客
  • 博客访问: 190202
  • 博文数量: 43
  • 博客积分: 2190
  • 博客等级: 大尉
  • 技术积分: 659
  • 用 户 组: 普通用户
  • 注册时间: 2009-02-04 16:15
文章分类
文章存档

2015年(1)

2013年(2)

2012年(1)

2011年(6)

2010年(11)

2009年(22)

分类: LINUX

2011-03-28 16:50:54

一、初始化阶段

    网络初始化被调用的路径为:

init->do_basic_setup->do_initcalls->net_olddevs_init->ethif_probe2->probe_list2->cs89x0_probe->cs89x0_probe1

真是不容易啊,终于进到cs89x0_probe1了,在这里开始探测和初始化cs8900了。下面就按照这个顺序来说明网络驱动第一阶段的工作。注意:这里的调用顺序是将cs8900驱动编入内核所产生的,如果将cs8900驱动选为模块,这个路径:init->do_basic_setup->do_initcalls->net_olddevs_init->ethif_probe2->probe_list2也会执行。

1.1 init函数

我们知道当start_kernel函数完成后就会启动init进程执行,在真正的应用程序init进程(如busybox/sbin/init)之前,Linux还需要执行一些初始化操作。init的代码可以在\init\main.c中找到,它的代码如下:

static int init(void * unused)
       {
               lock_kernel();
                ……                                                         //
省略多cpu的初始化代码先
                do_basic_setup();                                   //
我们所关注的初始化函数
                ……

        if (!ramdisk_execute_command)
                    ramdisk_execute_command = "/init";

      if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0)    {
                     ramdisk_execute_command = NULL;
                     prepare_namespace();                                       //
挂接根文件系统     
             }
             ……

free_initmem();                                                       //释放初始化代码的空间

       unlock_kernel();

……                                                                      //这几段没看懂

       if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) //检查控制台console是否存在

              printk(KERN_WARNING "Warning: unable to open an initial console.\n");

……//这几段没看懂

       if (ramdisk_execute_command) {           //运行ramdisk_execute_command指定的init用户进程

              run_init_process(ramdisk_execute_command);

              printk(KERN_WARNING "Failed to execute %s\n",

                            ramdisk_execute_command);

       }

       ……

       if (execute_command) {       //判断在启动时是否指定了init参数,如果指定,此值将赋给execute_command

              run_init_process(execute_command);              //开始执行用户init进程,如果成功将不会返回。

              printk(KERN_WARNING "Failed to execute %s. Attempting "

                                   "defaults...\n", execute_command);

       }

//如果没有指定init启动参数,则查找下面的目录init进程,如果找到则不会返回

       run_init_process("/sbin/init");

       run_init_process("/etc/init");

       run_init_process("/bin/init");

       run_init_process("/bin/sh");

   //如果上面的程序都出错,则打印下面的信息,如果内核找到init进程,则程序不会指向到此处

       panic("No init found. Try passing init= option to kernel.");

}

1.2 do_basic_setup函数

在这里我们最关心的是do_basic_setup函数,顾名思义该函数的功能就是“做基本设置”,它的实现代码也在\init\main.c中。do_basic_setup()完成外设及其驱动程序的加载和初始化。该函数代码如下所示:

static void __init do_basic_setup(void)

{

       /* drivers will send hotplug events */

       init_workqueues();     //初始化工作队列

       usermodehelper_init(); //初始化khelper内核线程,还没弄清楚

       driver_init();           //初始化内核的设备管理架构需要的数据结构,很复杂,以后在谈这部分。

#ifdef CONFIG_SYSCTL

       sysctl_init();          //没搞懂

#endif

       do_initcalls();         //重点函数,初始化的主要工作就靠它了

}

1.3 do_ initcalls函数

       do_initcalls函数将会调用内核中所有的初始化函数,它的代码同样在\init\main.c中。do_initcalls函数调用其他初始化函数相当简洁,它的关键代码如下所示:

initcall_t *call;

for (call = __initcall_start; call < __initcall_end; call ) {

……

       result = (*call)();

……

       简洁归简洁,但这段代码是什么意思呢?这说来就话长了,最重要的应该是先了解Linux处理初始化的大体思想,由于Linux有很多部分需要初始化,每个部分都有自己的初始化函数,如果按照常理一个一个的调用未免显得冗长,而且也不便于扩展。那么Linux是怎么处理的呢?首先,Linux将各个部分的初始化函数编译到一个块内存区中,当初始化完了以后释放这块内存区,这就是init函数中free_initmem所要做的事。然后,再在另外的内存区中存放一些函数指针,让每个指针指向一个初始化函数。然后在do_initcalls中依次根据这些指针调用初始化函数。

上面一段就是Linux实现初始化的大体思想,下面我们看看它最终是怎么实现的。首先要了解的是__define_initcall宏,该宏的定义在\ include\linux\init.h中,它的原型如下所示:

#define __define_initcall(level,fn,id) static initcall_t __initcall_##fn##id __attribute_used__ \

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

__define_initcall宏有三个参数,level表示初始化函数的级别,level值的大小觉得了调用顺序,level越小越先被调用,fn就是具体的初始化函数,id简单标识初始化函数,现在还没找到有什么用^_^__define_initcall的功能为,首先声明一个initcall_t类型的函数指针__initcall_##fn##idinitcall_t的原型为:

typedef int (*initcall_t)(void);

该类型可简单理解为函数指针类型^_^。然后,让该函数指针指向fn。最后,通过编译器的编译参数将此指针放到指定的空间".initcall" level ".init"中,__attribute_used向编译器说明这段代码有用,即使在没用到的时候,编译器也不会警告。__attribute____section__参数表示该段代码放入什么内存区域中,也即指定编译到什么地方,编译参数更详细的地方可以查阅GCC文档,在gcc官方网站中能找到各个版本的手册。这样说来还是比较抽象,下面举个例子来说明:

       假如有初始化函数init_foolish函数,现在使用__define_initcall宏向内核加入该函数。假如调用方式如下:

__define_initcall("0",init_foolish,1)

那么,__define_initcall宏首先申请一个initcall_t类型的函数指针__initcall_init_foolish1(注意替换关系),且使该指针指向了init_foolish,函数指针__initcall_init_foolish1被放到.initcall.0.init内存区域中,这个标志在连接时会用到。

       有了上面的基础知识,现在回到do_initcalls函数中,首先注意到是__initcall_start__initcall_end,它们的作用就是界定了存放初始化函数指针区域的起始地址,也即从__initcall_start开始到__initcall_end结束的区域中存放了指向各个初始化函数的函数指针。换句话说,只要某段程序代码从__initcall_start开始依次调用函数指针,那么就可以完成各个部分的初始化工作,这显得十分优雅而且便于扩充,再看看do_initcalls,它何尝不是如此呢。这里还有一个有用的技巧就是__initcall_start__initcall_end的原型是initcall_t型的数组,以后可以使用这种技巧^_^

       现在我们知道了do_initcalls函数的实现原理,那么到底它调用了多少初始化函数呢?我们怎样才能知道呢?根据上面的分析,我们知道所有的初始化函数的指针都放在__initcall_start__initcall_end区域期间,而函数指针与它指向的函数之间又有固定的关系,如上面的例子,初始化函数名为init_foolish,指向它的函数指针就是__initcall_init_foolish1,即在此函数加上前缀__initcall_和一个数字后缀,反之,从函数指针也可推出初始化函数名。有了这两个信息,我们就可以很方便的找个初始化函数。怎么找呢??首先打开Linux完后产生的System.map文件,然后找到__initcall_start__initcall_end字符串,你会发现它们之间有很多类似于__initcall_xxx1这样的符号,这些符号就是我们需要的函数指针了,这样就可推出初始化函数的名字。比如,我们这里需要的函数指针__initcall_net_olddevs_init6,按照上面的名字规则,很容易推出它所指向的初始化函数名字是net_olddevs_init

       得到了初始化函数的名字又怎么样呢?又不知道它在哪个文件里,不要着急!请打开你的浏览器登陆网站,然后选择Linux版本和架构,然后可以搜索我们想要的信息。比如我输入net_olddevs_init,然后我就会得到该函数所在文件的相关信息。

1.4 net_olddevs_init函数

       我们知道net_olddevs_init函数在do_initcalls函数中被调用并执行,那么它到底要做什么呢?看看实现代码就知道了,它的实现代码可以在\drivers\net\Space.c中找到。对于网络驱动部分的主要实现代码如下:

static int __init net_olddevs_init(void){  

……

       int num;

       for (num = 0; num < 8; num)

              ethif_probe2(num);

       ……

}

这段代码就不用讲解了吧,嘿嘿!就是调用了8ethif_probe2,赶快去看看ethif_probe2长什么样子。

1.5 ethif_probe2函数

       先看看该函数的实现代码,该代码也在\drivers\net\Space.c文件中。

static void __init ethif_probe2(int unit)

{

       unsigned long base_addr = netdev_boot_base("eth", unit);   // 由于ethif_probe2net_olddevs_init调用了8次,

                                          // 所以unit的值为07,也即在这里可以注册eth0eth7八个网络设备

       if (base_addr == 1)

              return;


       (void)(    probe_list2(unit, m68k_probes, base_addr == 0) &&

              probe_list2(unit, eisa_probes, base_addr == 0) &&

              probe_list2(unit, mca_probes, base_addr == 0) &&

              probe_list2(unit, isa_probes, base_addr == 0) &&

              probe_list2(unit, parport_probes, base_addr == 0));

}

       该函数首先调用netdev_boot_base所给的设备是否已经向内核注册,如果已注册netdev_boot_base返回1,随后推出ethif_probe2。如果设备没注册,则又调用函数probe_list2四次,每次传递的传输不同,注意到每次传递的第二个参数不同,这个参数也是相当重要的,这里拿isa_probes参数为例说明,因为这个参数与cs89x0_probe有关,isa_probes的定义也在\drivers\net\Space.c中,它的样子形如:

static struct devprobe2 isa_probes[] __initdata = {

……

#ifdef CONFIG_SEEQ8005

       {seeq8005_probe, 0},

#endif

#ifdef CONFIG_CS89x0

     {cs89x0_probe, 0},

#endif

#ifdef CONFIG_AT1700

       {at1700_probe, 0},

#endif

       {NULL, 0},

……

};

如果把cs8900的驱动选为非编译进内核,那么它的探测函数cs89x0_probe就不会存在于isa_probes数组中,所以在初始阶段就不能被调用。从上面的代码可以知道devprobe2类型至少包括两个域,至少一个域为函数指针,看看它的原型如下:

struct devprobe2 {

       struct net_device *(*probe)(int unit);                         //函数指针,指向探测函数

       int status;       /* non-zero if autoprobe has failed */

};

下面看看probe_list2函数是怎么表演的。

1.6 ethif_probe2函数

       对于ethif_probe2函数也没有什么需要说明的,它的主要任务是依次调用devprobe2类型的probe域指向的函数。他的实现代码同样在\drivers\net\Space.c中,它的关键代码如下:

static int __init probe_list2(int unit, struct devprobe2 *p, int autoprobe)

{

       struct net_device *dev;

       for (; p->probe; p ) {

           ……

              dev = p->probe(unit);

              ……

       }

……

}

1.7 cs89x0_probe函数

       从该函数起,真正开始执行与cs8900驱动初始化程序,该函数在\drivers\net\cs89x0.c文件实现。下面依次解释该函数。

struct net_device * __init cs89x0_probe(int unit)

{

       struct net_device *dev = alloc_etherdev(sizeof(struct net_local)); //该函数申请一个net_device

//sizeof(struct net_local)的空间,net_localcs8900驱动的私有数据空间。

       unsigned *port;

       int err = 0;

       int irq;

       int io;

      

       if (!dev)

              return ERR_PTR(-ENODEV);

       sprintf(dev->name, "eth%d", unit);                 //初始化dev->name

       netdev_boot_setup_check(dev);                  //检查是否给定了启动参数,如果给定了启动参数,此函数将初始

//devirqbase_addrmem_startmem_end域。

       io = dev->base_addr;                                 //io实际实质cs8900所占地址空间的起始地址,此地址为虚拟地址

       irq = dev->irq;

       if (net_debug)

              printk("cs89x0:cs89x0_probe(0x%x)\n", io);

//下面根据io的值调用cs89x0_probe1函数

       if (io > 0x1ff) {/* Check a single specified location. *///此段没搞懂,由于没给启动参数,这里也不会执行

       err = cs89x0_probe1(dev, io, 0);

       } else if (io != 0) { /* Don''''''''''''''''''''''''''''''''t probe at all. */

              err = -ENXIO;

       } else {

              for (port = netcard_portlist; *port; port ) {// netcard_portlistunsigned int型数组,在cs89x0.c文件中定

//义,里面列出了cs8900可能占用空间的起始地址,这些地址

//将在cs89x0_probe1函数中用于向内核申请。

                     if (cs89x0_probe1(dev, *port, 0) == 0) // cs89x0_probe1探测成功就返回0

                            break;

                     dev->irq = irq;

              }

              if (!*port)

                     err = -ENODEV;

       }

       if (err)

              goto out;

       return dev;

out:

       free_netdev(dev);   //表示探测失败,这里就释放dev的空间,随后打印些消息

       printk(KERN_WARNING "cs89x0: no cs8900 or cs8920 detected. Be sure to disable PnP with SETUP\n");

       return ERR_PTR(err);

}

       从上面的程序清单可以看到该函数还没有真正的开始探测cs8900,实质的探测工作是让cs89x0_probe1完成的。在解释cs89x0_probe1之前先提一下网络驱动程序中非常重要的一些函数。内核需要一个数据结构来管理或者描述每个网络驱动程序,这个数据类型就是struct net_device,该数据类型包括很多域,详细的解释可以参见《Linux 设备驱动程序》一书中的描述,也可以参见源代码(在\include\linux\netdevice.h中,源码中也有详细的注解)。内核为了编程方便特地实现了函数alloc_netdev来完成对net_device的空间分配。那么alloc_etherdev函数主要针对以太网在alloc_netdev基础上封装的一个函数,它除了申请net_device空间外,还会初始化net_device的相关域。

三、net_rx和net_send_packet
3.1 net_rx
在这部分将介绍cs8900驱动的两个最重要的函数,内核通过该两个函数实现了数据的收发。net_rx函数的主要功能是从cs8900的片上数据缓冲区中将数据传送给sk_buff缓冲区,sk_buff是网络驱动程序与Linux内核通信的缓冲区。该结构可在\include\linux\skbuff.h中找到。net_rx函数的功能可总结如下:(该总结来源于:)
A.获取私有数据存放于lp中;
B.获取设备缓冲区状态和缓冲长度;
C.如果状态不为RX_OK则计数接收数据错误次数count_rx_error()
D.分配一个sk_buf区间
E.字对齐,skb_reserve();
F.插入数据到接收口,insw();
G.写入数据;
H.初始化sk_buff结构,eth_type_trans()
I.进入上层接收函数netif_rx();
J.初始化设备的计数;
net_rx函数的注解如下所示:
static void net_rx(struct net_device *dev)
{
       struct net_local *lp = netdev_priv(dev);             //lp指向驱动程序的私有数据区
       struct sk_buff *skb;                             //申请skb_buff指针
       int status, length;

       int ioaddr = dev->base_addr;                       // 得到cs8900的基地址
       status = readword(ioaddr, RX_FRAME_PORT);       //获取cs8900片上缓冲区的状态
       length = readword(ioaddr, RX_FRAME_PORT);       //获取cs8900片上缓冲区的长度

       if ((status & RX_OK) == 0) {          //状态为接收错误,调用count_rx_errors统计错误
              count_rx_errors(status, lp);
              return;
       }

       /* Malloc up new buffer. */
       skb = dev_alloc_skb(length + 2); //分配一个缓冲区,dev_alloc_skb函数以
三、net_rxnet_send_packet
3.1 net_rx
在这部分将介绍cs8900驱动的两个最重要的函数,内核通过该两个函数实现了数据的收发。net_rx函数的主要功能是从cs8900的片上数据缓冲区中将数据传送给sk_buff缓冲区,sk_buff是网络驱动程序与Linux内核通信的缓冲区。该结构可在\include\linux\skbuff.h中找到。net_rx函数的功能可总结如下:(该总结来源于:)
A.获取私有数据存放于lp中;
B.获取设备缓冲区状态和缓冲长度;
C.如果状态不为RX_OK则计数接收数据错误次数count_rx_error()
D.分配一个sk_buf区间
E.字对齐,skb_reserve();
F.插入数据到接收口,insw();
G.写入数据;
H.初始化sk_buff结构,eth_type_trans()
I.进入上层接收函数netif_rx();
J.初始化设备的计数;

net_rx函数的注解如下所示:
static void net_rx(struct net_device *dev)
{
       struct net_local *lp = netdev_priv(dev);             //lp指向驱动程序的私有数据区
       struct sk_buff *skb;                             //申请skb_buff指针
       int status, length;

       int ioaddr = dev->base_addr;                       // 得到cs8900的基地址
       status = readword(ioaddr, RX_FRAME_PORT);       //获取cs8900片上缓冲区的状态
       length = readword(ioaddr, RX_FRAME_PORT);       //获取cs8900片上缓冲区的长度

       if ((status & RX_OK) == 0) {    //状态为接收错误,调用count_rx_errors统计错误
              count_rx_errors(status, lp);
              return;
       }

       /* Malloc up new buffer. */
       skb = dev_alloc_skb(length + 2);             //分配一个缓冲区,dev_alloc_skb函数以
                   //GFP_ATOMIC优先级调用alloc_skb。alloc_skb的功能为分配一个缓冲区
                   //并初始化skb->data,skb->tail和skb_head域。dev_alloc_skb和alloc_skb的区
                   //别为,前者在skb->data和skb_head之间保留了一些空间,网络层使用这
                   //一数据空间进行优化工作,驱动程序不该访问该空间。
       if (skb == NULL) {    //skb缓冲区分配失败?
……
              lp->stats.rx_dropped++;  //直接将丢包数加1
              return;
       }
       skb_reserve(skb, 2);      /* longword align L3 header */    //该函数增加skb的data和tail,
               //该函数可填充缓冲区之前保留报文头空间,大多数以太网在数据包之前
               //保留2个字节,这样IP头可在14字节的以太网头之后,在16字节边界上对
               //齐。这里也空了两个字节,这两个自己加上14字节的以太网头刚好16字
              //节。所以这里的主要作用是字对齐。
       skb->dev = dev;

       readwords(ioaddr, RX_FRAME_PORT, skb_put(skb, length), length >> 1);  //skb_put函
            //数的作用是更新skb的tail和len成员,也即在缓冲区尾部添加数据,该函数返
            //回skb->tail的先前值。整句代码的含义为,从cs8900的数据缓冲区中读取
      //length个字节数据到skb缓冲区。由于readwords是以读取字(两个字节)为
            //单位,所以length应该保持字对齐,也即length右移一位。
       if (length & 1)       //因为前面length以字对齐,如果length为单字节,
                                   //所以这里应该补上最后一个字节
              skb->data[length-1] = readword(ioaddr, RX_FRAME_PORT);
……
        skb->protocol=eth_type_trans(skb,dev);  //该函数定义在linux/net/ethernet/eth.c中,
                                                 //该处可参见linux设备驱动程序相关章节
       netif_rx(skb);       //通知内核已经接收到一个数据包,并封装入一个套接字缓冲区
       dev->last_rx = jiffies;                    //更新最后的接收包时间
       lp->stats.rx_packets++;                //接收的总数据包数加1
       lp->stats.rx_bytes += length;         //接收的字节数加上length
}

3.1 net_send_packet
       net_send_parcket为内核提供了数据包发送功能,该函数在cs89x0_probe1中被赋予了net_device的hard_start_xmit域,当内核需要发送数据包时,将调用dev-> hard_start_xmit完成最后的数据包发送。该函数被调用的前提是,在调用该函数之前,内核已经将数据包放入了skb缓冲区中。该函数的主要任务有:
A.获取设备私有数据指针
B.加环形锁,spin_lock_irq();
C.检测缓冲区是否为满,若满则调用netif_stop_queue()暂停发送队列;
D.写发送命令和发送长度,writeword();
E.读取发送总线状态readreg();
F.解环形锁,spin_unlock_irq();
G.设置传输时钟计数;
H.释放相应sk_buff, dev_kfree_skb().
下面为此函数的简单注释:
static int net_send_packet(struct sk_buff *skb, struct net_device *dev)
{
       struct net_local *lp = netdev_priv(dev);            //获得驱动程序的私有数据

……

       spin_lock_irq(&lp->lock);         //获得自旋锁,以便进入临界区
       netif_stop_queue(dev);    //通知内核暂停内核与驱动程序间的数据传递,也即告诉
                                              //内核不要向skb缓冲区填充数据。

       /* initiate a transmit sequence */  //初始化cs8900的发送对列,主要为写命令和数
                                                         //据长度,为数据发送做准备
       writeword(dev->base_addr, TX_CMD_PORT, lp->send_cmd);
       writeword(dev->base_addr, TX_LEN_PORT, skb->len);

       /* Test to see if the chip has allocated memory for the packet * /       //查看cs8900是否为
                                                                                      //发送分配了地址空间。
       if ((readreg(dev, PP_BusST) & READY_FOR_TX_NOW) == 0) {
           …….
              spin_unlock_irq(&lp->lock);
              if (net_debug) printk("cs89x0: Tx buffer not free!\n");
              return 1;
       }
       /* Write the contents of the packet */
       writewords(dev->base_addr, TX_FRAME_PORT,skb->data,(skb->len+1) >>1);  //将数
                                                                                                               //据交给cs8900发送
       spin_unlock_irq(&lp->lock);         //发送结束,释放自旋锁
       lp->stats.tx_bytes += skb->len;    //累加发送的总字节数
       dev->trans_start = jiffies;             //更新最后的传输时间
       dev_kfree_skb (skb);                  //发送完毕,释放skb缓冲区
    ……
       return 0;
}
总结:
       在cs8900驱动中,主要简解了驱动程序中的部分重要函数,包括初始化、打开/关闭网络驱动和发送/接收数据。对于其余的驱动程序代码,如超时处理、状态获取等函数没做解释,它们的实现也比较简单。由于自己板子上没有EEPROM,所以也没有分析与EEPROM相关部分的代码。DMA部分好像编译进去会错,所以也没有去分析,以后有时间再去弄弄DMA部分。
                                                                                             

阅读(1338) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~