- 论坛徽章:
- 0
|
<!-- [/td][td][/td][/tr][/table]1 前言 \r\n\r\n许多人在分析linux代码时对网络部分(主要是src/linux/net,src/linux/include/net及 \r\nsrc/linux/include/linux目录下的文件)比较感兴趣,确实,尽管已经从书本上学到了大 \r\n量的TCP/IP原理,不读源码的话,头脑中还是建立不起具体的印象。而分析这部分代码的 \r\n一个问题便是代码众多而资料很少。这篇文章的目的就是勾勒出一个框架,让读者能够大致 \r\n能够了解TCP/IP究竟是怎么工作的。以前见到的许多代码分析都是基于2.0内核的,在新的 \r\n内核中许多变了名字,这尤其给初学者带来了困难,本文是以2.4.0-test9的代码作例子, \r\n这样对照代码时可能更清晰些。 \r\n\r\n其实网络部分的代码我只对防火墙部分一行行仔细分析过,其他许多地方也只是一知半解, \r\n如果理解有误,欢迎指正。 \r\n\r\n建议在看本文的同时,用source insight(www.soucedyn.com)建立一个项目,同时看代码, \r\n这样可能效果更好点。我也用过其他的一些工具,但在分析大量的代码的时候,没有一个工 \r\n具比它更方便的了。 \r\n\r\n\r\n2 正文 \r\n\r\nISO的七层模型都非常熟悉了,当然,对于internet,用四层模型更为适合。在这两份模型里, \r\n网络协议以层次的形式出现。而LINUX的内核代码中,严格分出清楚的层次却比较困难,因为 \r\n除了一些\"内核线程(kernel thread外)\",整个内核其实是个单一的进程。因此所谓\"网络层\" \r\n,只是一组相关的函数,而各层之间大多通过一般的调用的方式完成交互。 \r\n\r\n而从逻辑上,网络部分的代码更应该这样分层更为合理: \r\n.BSD socket层:这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。 \r\n这一部分的文件主要有:/net/socket.c /net/protocols.c etc \r\n.INET socket层:BSD socket是个可以用于各种网络协议的,而当用于tcp/ip,即建立了AF_INET \r\n形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。 \r\n文件主要有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc \r\n.TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示。 \r\n文件主要有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c \r\n/net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c \r\n/net/ipv4/tcp_timer.c etc \r\n.IP层:处理网络层的操作,网络层用struct packet_type结构表示。 \r\n文件主要有:/net/ipv4/ip_forward.c ip_fragment.c ip_input.c ip_output.c etc. \r\n.数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中, \r\n驱动程序都在/driver/net目录下。 \r\n\r\n网络部分还有很多其他文件,如防火墙,路由等,一般根据看到名字便能猜测出相应的处理,此处不再赘述。 \r\n\r\n现在我要给出一张表,全文的内容就是为了说明这张表(如果你觉得我在文章中的语言比较乏味,尽可 \r\n抛掉他们,结合这张表自己看代码)。在我最初看网络部分代码时,比较喜欢《linux kernel internals》 \r\n的第八章的一段,其中有一个进程A通过网络远程向另一进程B发包的例子,详细介绍了一个数据包如何 \r\n从网络堆栈中走过的过程。我觉得这样可以更迅速的帮助读者看清森林的全貌,因此本文参照这种结构来 \r\n叙述。 \r\n\r\n^ \r\n| sys_read fs/read_write.c \r\n| sock_read net/socket.c \r\n| sock_recvmsg net/socket.c \r\n| inet_recvmsg net/ipv4/af_inet.c \r\n| udp_recvmsg net/ipv4/udp.c \r\n| skb_recv_datagram net/core/datagram.c \r\n| ------------------------------------------- \r\n| sock_queue_rcv_skb include/net/sock.h \r\n| udp_queue_rcv_skb net/ipv4/udp.c \r\n| udp_rcv net/ipv4/udp.c \r\n| ip_local_deliver_finish net/ipv4/ip_input.c \r\n| ip_local_deliver net/ipv4/ip_input.c \r\n| ip_recv net/ipv4/ip_input.c \r\n| net_rx_action net/dev.c \r\n| ------------------------------------------- \r\n| netif_rx net/dev.c \r\n| el3_rx driver/net/3c309.c \r\n| el3_interrupt driver/net/3c309.c \r\n\r\n========================== \r\n\r\n| sys_write fs/read_write.c \r\n| sock_writev net/socket.c \r\n| sock_sendmsg net/socket.c \r\n| inet_sendmsg net/ipv4/af_inet.c \r\n| udp_sendmsg net/ipv4/udp.c \r\n| ip_build_xmit net/ipv4/ip_output.c \r\n| output_maybe_reroute net/ipv4/ip_output.c \r\n| ip_output net/ipv4/ip_output.c \r\n| ip_finish_output net/ipv4/ip_output.c \r\n| dev_queue_xmit net/dev.c \r\n| -------------------------------------------- \r\n| el3_start_xmit driver/net/3c309.c \r\nV \r\n\r\n\r\n\r\n我们假设的环境如下:有两台主机通过互联网联在一起,其中一台机子运行这一个进程A, \r\n另外一台运行进程B,进程A将向进程B发出一条信息,比如\"Hello\",而B接受此信息。 \r\nTCP处理本身非常复杂,为了便于叙述,在后面我们将用UDP作为例子。 \r\n\r\n\r\n2.1 建立套接字 \r\n\r\n在数据发送之前,要建立一个套接字(socket),在两边的程序中都会调用如下语句: \r\n\r\n... \r\nint sockfd; \r\nsockfd=socket(AF_INET,SOCK_DGRAM,0); \r\n... \r\n\r\n这是个系统调用,因此会通过0x80中断进入系统内核,调用内核中的相应.当寻找 \r\n系统调用在内核中的对应流程时,一般前面加入\"sys_\"再找就是了,如对fork来说,就是 \r\n调用sys_fork。但是socket相关调用有些特殊,所有的这类调用都是通过一个入口,即 \r\nsys_socketcall进入系统内核,然后再通过参数调用具体的sys_socket,socket_bind等。 \r\n\r\nsys_socket会调用sock_create产生一个struct socket结构(见include/linux/net.h), \r\n每个套接字在内核中都有一个这样的结构对应,在初始化了此结构的一些通用成员后(如 \r\n分配inode,根据第二个参数为type项赋值等),会根据其一个参数作响应的调度,即这 \r\n一句: \r\n... \r\nnet_families[family]->create(sock, protocol); \r\n... \r\n\r\n我们的程序的第一个参数是AF_INET,所以此会指向inet_create();(net_families \r\n是个数组,保留了网络协议族(net families)的信息,而这些协议族用sock_register加载。) \r\n\r\n在struct socket结构结构中最重要的信息保留在struct sock结构中,这个结构在网络代码中经常 \r\n使用,建议把它和其他常见结构(如struct sk_buff)打印出来放在手边。在inet_create会为此 \r\n结构分配内存,并根据套接字类型(其实就是socket的第二个参数),作各自不同的初始化: \r\n... \r\nif (sk->prot->init) \r\nsk->prot->init(sk); \r\n... \r\n\r\n如果类型是SOCK_STREAM的话会调用tcp_v4_init_sock,而SOCK_DGRAM类型的socket没有额外的初始化了, \r\n到此socket调用结束。 \r\n\r\n还有一个值得注意的地方是当inet_create()调用完后,会接着调用sock_map_fd,这个 \r\n中会为套接字分配一个文件描述符并分配一个file文件。在应用层便可象处理文件一样 \r\n处理套接字了。 \r\n\r\n开始的时候可能有些流程难以跟下去,主要便是这些的实际指向会根据类型变化。 \r\n\r\n\r\n2.2 发送数据 \r\n\r\n当进程A想发送数据时,程序中会调用如下语句(如果用send的话会走类似的流程,略): \r\n... \r\nwrite(sockfd,\"Hello\",strlen(\"Hello\" ); \r\n... \r\n\r\nwrite在内核中对应的函数就是sys_write,此首先根据文件描述符找到struct file结构,如果此文件 \r\n存在(file非空)且可写(file->f_mode & FMODE_WRITE为true),便调用此文件结构的写操作: \r\n... \r\nif (file->f_op && (write = file->f_op->write) != NULL) \r\nret = write(file, buf, count, &file->f_pos); \r\n... \r\n\r\n其中f_op是个struct file_operations结构,在sock_map_fd中将其指向socket_file_ops,其 \r\n定义如下(/net/socket.c): \r\nstatic struct file_operations socket_file_ops = { \r\nllseek: sock_lseek, \r\nread: sock_read, \r\nwrite: sock_write, \r\npoll: sock_poll, \r\nioctl: sock_ioctl, \r\nmmap: sock_mmap, \r\nopen: sock_no_open, /* special open code to disallow open via /proc */ \r\nrelease: sock_close, \r\nfasync: sock_fasync, \r\nreadv: sock_readv, \r\nwritev: sock_writev \r\n}; \r\n\r\n此时wirte函数显然指向了sock_write,我们跟下去看,此将一个字符串缓冲整理成struct msghdr, \r\n最后调用了sock_sendmsg. \r\n\r\nsock_sendmsg中的scm_send我不了解(scm是Socket level control messages的简写),好在它也不是很关键, \r\n我们注意到这句: \r\n... \r\nsock->ops->sendmsg(sock, msg, size, &scm); \r\n... \r\n\r\n又是个函数,sock->ops在inet_create()中被初始化,由于我们我们是UDP的套接字,sock->ops \r\n指向了inet_dgram_ops(即sock->ops = &inet_dgram_ops;),其定义在net/ipv4/Af_inet.c中: \r\nstruct proto_ops inet_dgram_ops = { \r\nfamily: PF_INET, \r\n\r\nrelease: inet_release, \r\nbind: inet_bind, \r\nconnect: inet_dgram_connect, \r\nsocketpair: sock_no_socketpair, \r\naccept: sock_no_accept, \r\ngetname: inet_getname, \r\npoll: datagram_poll, \r\nioctl: inet_ioctl, \r\nlisten: sock_no_listen, \r\nshutdown: inet_shutdown, \r\nsetsockopt: inet_setsockopt, \r\ngetsockopt: inet_getsockopt, \r\nsendmsg: inet_sendmsg, \r\nrecvmsg: inet_recvmsg, \r\nmmap: sock_no_mmap, \r\n}; \r\n\r\n因此我们要看得便是inet_sendmsg()函数了,而马上,这个函数又通过函数调用了另一: \r\n... \r\nsk->prot->sendmsg(sk, msg, size); \r\n... \r\n\r\n我们不得不再次寻找其具体指向。看到这里,说点题外话,怎么才能找到其具体定义呢?我一般是这样: \r\n对上例而言,sk是个struct sock结构,到其定义(linux/net/sock.h中)出看到prot是个struct proto \r\n结构,此时我们便在源代码树中寻找所有此结构的实例(这些诸如跳到定义,寻找引用等工作在source \r\ninsight中实在太方便快速了^_^),很快便会发现诸如udp_prot,tcp_prot,raw_prot等,猜测是用了 \r\nudp_prot,便再找一下它在源代码中的引用情况,果然发现在inet_create中有这么一句: \r\n... \r\nprot=&udp_prot; \r\n... \r\n\r\n其实如果前面看inet_create时仔细一点会早点发现了,但我总没有这么细心 。 |
|