天天看点

linux0.99网络模块-网络模块初始化

linux0.11版内核的启动过程我们已经分析过了,具体可以参考《系统引导源码分析bootsect.s》《setup.s源码分析》《head.s源码分析》《init/main.c源码分析》四篇文章,对于0.99版稍有不同。我们看一下head.S中的改变。

linux0.11内核head.s:

135     after_page_tables:

136     pushl $0        # These are the parameters to main :-)

137     pushl $0

138     pushl $0

139     pushl $L6       # return address for main, if it decides to.

140     pushl $_main

141     jmp setup_paging

linux0.99内核head.s

 81     lss _stack_start,%esp

 82     pushl $0        # These are the parameters to main :-)

 83     pushl $0

 84     pushl $0

 85     cld         # gcc2 wants the direction flag cleared at all times

 86     call _start_kernel

 87 L6:

 88     jmp L6          # main should never return here, but

 89                 # just in case, we know what happens.

可见linux0.99内核head.S中调用的不再是main函数,而是start_kernel.我们来看一下它的源码。

202 void start_kernel(void)

203 {              ..................

226     trap_init();

227     init_IRQ();

228     sched_init();

229     parse_options(command_line);

42      buffer_init();

243     inode_init();

244     time_init();

245     floppy_init();

246     sock_init();

247     sti();

         ........................

260     for(;;)

261         idle();

262 }

可以看到246行正是初始化网络模块的。我们来看一下:

net/socket.c

944 void

945 sock_init(void)

946 {

947     struct socket *sock;

948     int i, ok;

949

950     for (sock = sockets; sock <= last_socket; ++sock)

951         sock->state = SS_FREE;

952     for (i = ok = 0; i < NPROTO; ++i) {

953         printk("sock_init: initializing family %d (%s)\n",

954                proto_table[i].family, proto_table[i].name);

955         if ((*proto_table[i].ops->init)() < 0) {

956             printk("sock_init: init failed.\n",

957                    proto_table[i].family);

958             proto_table[i].family = -1;

959         }

960         else

961             ++ok;

962     }

963     if (!ok)

964         printk("sock_init: warning: no protocols initialized\n");

965     return;

966 }

下面我们逐步来分析该函数:

945 sock_init(void)

946 {

947     struct socket *sock;

948     int i, ok;

950     for (sock = sockets; sock <= last_socket; ++sock)

951         sock->state = SS_FREE;

 75 static struct socket sockets[NSOCKETS]; 其中NSOCKETS是net/kern_sock.h中的宏定义,值为128

net/kern_sock.h:4:#define NSOCKETS 128            

也就是说sockets是一个大小为128的socket数组。

950行-951行的for循环用于初始化sockets中的每个socket为空闲状态。

继续回到sock_init(void)函数

952     for (i = ok = 0; i < NPROTO; ++i) {

953         printk("sock_init: initializing family %d (%s)\n",

954                proto_table[i].family, proto_table[i].name);

955         if ((*proto_table[i].ops->init)() < 0) {

956             printk("sock_init: init failed.\n",

957                    proto_table[i].family);

958             proto_table[i].family = -1;

959         }

960         else

961             ++ok;

962     }

其中NRPROTO定义于net/socket.c:35:#define NPROTO (sizeof(proto_table) / sizeof(proto_table[0]))

来看一下proto_table的定义:

 25 static struct {

 26     short family;

 27     char *name;

 28     struct proto_ops *ops;

 29 } proto_table[] = {

 30     {AF_UNIX,   "AF_UNIX",  &unix_proto_ops},

 31 #ifdef CONFIG_TCPIP

 32     {AF_INET,   "AF_INET",  &inet_proto_ops},

 33 #endif

 34 };

对应下图:

linux0.99网络模块-网络模块初始化

NRPROTO对应的是协议的数目。

因此这里的for循环遍历每一个协议,953行打印正在遍历的协议族编号(名称),955行调用当前协议的初始化函数,如果初始化失败,打印出错信息,并将其family赋值为-1.如果初始化正常,则递增ok来计数。

继续

963     if (!ok)

964         printk("sock_init: warning: no protocols initialized\n");

965     return;

966 }

963行如果没有一个协议被初始化,965行打印信息。

到这里初始化函数就分析完了。

接下来,我们就TCP/IP协议来具体看一下它的初始化过程。上面proto_table对应的第2项就是TCP/IP协议,它注册的操作函数为inet_proto_ops。我们看一下它的init方法。

net/tcp/sock.c

 156 struct proto_ops inet_proto_ops =

 157 {

 158   ip_proto_init,

 159   ip_proto_create,

 160   ip_proto_dup,

 161   ip_proto_release,

 162   ip_proto_bind,

 163   ip_proto_connect,

 164   ip_proto_socketpair,

 165   ip_proto_accept,

 166   ip_proto_getname,

 167   ip_proto_read,

 168   ip_proto_write,

 169   ip_proto_select,

 170   ip_proto_ioctl,

 171   ip_proto_listen,

 172   ip_proto_send,

 173   ip_proto_recv,

 174   ip_proto_sendto,

 175   ip_proto_recvfrom,

 176   ip_proto_shutdown,

 177   ip_proto_setsockopt,

 178   ip_proto_getsockopt,

 179   ip_proto_fcntl

 180 };

首先我们看到inet_proto_ops是一个方法的集合,这与驱动编写中的file_operations非常相似。现在还是重点来看它的init方法,它对应的就是158行的ip_proto_init。

net/tcp/sock.c

我们分段来看

 835

 836 static int ip_proto_init(void)

 837 {

 838   int i;

 839   struct device *dev, *dev2;

 840   struct ip_protocol *p;

 841   seq_offset = CURRENT_TIME*250;

 842  

 843   for (i = 0; i < SOCK_ARRAY_SIZE; i++)

 844     {

 845        tcp_prot.sock_array[i] = NULL;

 846        udp_prot.sock_array[i] = NULL;

 847        raw_prot.sock_array[i] = NULL;

 848     }

845-848行初始化了几个相关数组,元素值均设为NULL。关于这里的tcp_prot,udp_prot,raw_prot。以tcp_prot为例,它的定义于net/tcp/tcp.c:3185 struct proto tcp_prot。我们看一下proto结构体:

100 struct proto

101 {

102   void *(*wmalloc)(volatile struct sock *sk, unsigned long size, int force,

103            int priority);

.........省略.....

139   unsigned short max_header;

140   unsigned long retransmits;

141   volatile struct sock *sock_array[SOCK_ARRAY_SIZE];

142 };

根据名字就可以知道proto是协议的结构体,它包含大量的函数指针,这里只看一下sock_array,它是一个sock数组,可以先大致说一下(详细分析看传输层源码,比如《linux0.99网络模块-传输层(TCP接收)》),sock对应一个TCP连接(它是一个四元组源IP:源端口 目的IP:目的端口),为什么要以数组的形式存在呢?这也是从效率和管理角度考虑的,我们知道TCP连接会指明目的端口,这里就根据目的端口号进行哈希后保存到相应的索引对应的链表上。

继续回到前面分析:

 850   for (p = ip_protocol_base; p != NULL;)

 851     {

 852        struct ip_protocol *tmp;

 853       

 854        tmp = p->next;

 855        add_ip_protocol (p);

 856        p = tmp;

 857     }

先来看一下ip_protocol_base:

net/tcp/protocols.c:

 86 struct ip_protocol *ip_protocol_base = &icmp_protocol;

 76 static struct ip_protocol icmp_protocol =

 77 {

........省略........

 80    &udp_protocol, ........省略........

 84 };

再来看一下ip_protocol的定义

net/tcp/ip.h

130 struct ip_protocol

131 {

.......省略.......

138    struct ip_protocol *next;

.......省略.......

142 };

通过上面可以知道,ip_protocol_base指向了icmp_protocol的地址,icmp_protocol中又通过next指针指向了udp_protocol,而udp_protocol通过next指向了tcp_protocol(上面没有给出).

所以,850行开始的for循环,依次对icmp_protocol,udp_protocol和tcp_protocol调用add_ip_protocol。

我们来看一下这个方法:

net/tcp/ip.c

100 void

101 add_ip_protocol (struct ip_protocol *prot)

102 {

103    unsigned char hash;

104    struct ip_protocol *p2;

105    hash = prot->protocol & (MAX_IP_PROTOS-1);

106    prot ->next = ip_protos[hash];

107    ip_protos[hash] = prot; 这里就是把prot添加到ip_protos之中合适的位置,如果该位置上已经存在就链接成链

108    prot->copy = 0;

109   

110    for (p2 = prot->next; p2 != NULL; p2=p2->next)

111      {

112     if (p2->protocol == prot->protocol)

113       {

114          prot->copy = 1;

115          break;

116       }

117      } 如果这条链上有其他元素与刚刚添加的元素属于同一个协议(protocol字段相同),就置位它的copy标记,然后中断循环(因为后面的元素已经在其他元素插入时进行过同样的设置了)。那么这里的copy是什么用呢?分析到网络层的时候我们发现它是用来指明是否要为上层协议复制数据报的。也就是说,IP层收到数据报后应该向上层传输,但是如果有多个上层协议注册到了IP层(copy==1),这时就需要为它们复制数据报(更严谨的说,如果有n个上层协议注册到IP层,那么需要复制n-1个数据报)。

119 }

这里的ip_protos其实是用于保存注册到网络层中的传输层(也包括ICMP)协议,网络层在接收到数据报后会根据这里注册的协议向相应的协议传输数据报。《linux0.99网络模块-网络层(接收)》中的分析可以印证这句话,另外多说一句,链路层也是用了同样的思想,ip协议和arp协议注册到链路层来接收链路层的数据报,只不过那里是简单的链表,并没有使用hash。copy字段是用来表明是否需要拷贝一个数据报的副本来传给相应的协议,如果只有一个对象对此数据报感兴趣就不需要拷贝,如果存在多个就需要给每一个对象拷贝一个副本。

继续ip_proto_init的分析:

 859  

 860  

 863

 864   dev2 = NULL;

 865   for (dev = dev_base; dev != NULL; dev=dev->next)

 866     {

 867        if (dev->init && dev->init(dev))

 868      {     //下面是初始化失败的情况

 869        if (dev2 == NULL)

 870          dev_base = dev->next;

 871        else

 872          dev2->next = dev->next;     //删除该设备

 873      }

 874        else

 875      {

 876        dev2 = dev;    

 877      }

 878     }

如果设备初始化失败就从链上删除它(注意dev->init成功返回0,失败返回1),直到下次boot才会重新初始化它。

我们具体来看一下这里的dev_base,它定义于net/tcp/Space.c:

100 struct device *dev_base = &loopback_dev;

loopback_dev通过next指针链接了wd8003_dev,其中loopback_dev就是回环设备,而wd8003_dev就是网卡设备了。关于设备的初始化请看《linux0.99网络模块-网络设备初始化》。

继续:

 879   bh_base[INET_BH].routine = inet_bh;

 880   timer_table[NET_TIMER].fn = net_timer;

 881   return (0);

 882 }

bh_base定义于kernel/irq.c:42:struct bh_struct bh_base[32]; 879行设置了TCP协议的下半部处理函数。

timer_table定义于kernel/sched.c:350:struct timer_struct timer_table[32];

include/linux/timer.h

 37 struct timer_struct {

 38     unsigned long expires;

 39     void (*fn)(void);

 40 };

880行是设置的TCP协议的超时计时器的处理函数。

到此我们就把网络模块的初始化过程分析完了。

整个过程大致图示如下:

linux0.99网络模块-网络模块初始化

总结:

网络模块初始化工作是在sock_init函数中开始的。对于linux0.99来说它是init/main.c中的start_kernel函数中调用的。它首先把sockets数组中的所有socket的状态都初始化为空闲状态,然后遍历所有网络协议,并依次调用它们的init方法进行初始化。所有的网络协议都存在一个proto_table数组中,它的元素指明了协议族,协议名,以及操作函数(proto_ops),这里就是通过它来找到对应init方法的。TCP协议对应的初始化方法为ip_proto_init,在这个方法中初始化了序列号(seq_offset),并且把相关协议的sock_array(关于sock_array会在传输层会看到它的用处)每个元素初始化NULL,随后遍历所有的传输层协议(这么说不是很准确,总之就是准备注册到IP层的协议),并把它们加入到ip_protos数组中,这个数组类似与Java中的HashMap,如果加入到ip_protos中的协议被hash到了同一个位置,就会被链接成链,并且其后所有与其具有相同protocol字段的元素的copy属性都被置位(因为如果有多个对象对一个数据报感兴趣的话,就必须给每一个对象一份数据报拷贝),最后初始化设备.对于linux0.99来说,初始化了两个设备loopback和wd8003。关于初始化设备的具体过程见《linux0.99网络模块-网络设备初始化》,最后注册了中断下半部处理函数和超时处理函数。