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 };
对应下图:

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协议的超时计时器的处理函数。
到此我们就把网络模块的初始化过程分析完了。
整个过程大致图示如下:
总结:
网络模块初始化工作是在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网络模块-网络设备初始化》,最后注册了中断下半部处理函数和超时处理函数。