理解linux 7层RX TX

正文
 
ISO的七层模型都非常熟悉了,当然,对于internet,用四层模型更为适合。在这两份模 
型里, 网络协议以层次的形式出现。而LINUX的内核代码中,严格分出清楚的层次却比较困 难,因为 除了一些"内核线程(kernel thread)",整个内核其实是个单一的进程。因此所谓"网络层" ,只是一组相关的函数,而各层之间大多通过一般的函数调用的方式完成交互。 而从逻辑上,网络部分的代码更应该这样分层更为合理: 
.BSD socket
这一部分处理BSD socket相关操作, 每个socket在内核中以struct  socket结构体现。 这一部分的文件主要有:/net/socket.c   /net/protocols.c  etc 
 
.INET socket
BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即 建立了AF_INET  形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。  文件主要有:/net/ipv4/protocol.c  /net/ipv4/af_inet.c  /net/core/sock.c   etc 
 
.TCP/UDP层:
处理传输层的操作,传输层用struct inet_protocolstruct proto两个 结构表示。 
 文件主要有:/net/ipv4/udp.c   /net/ipv4/datagram.c    /net/ipv4/tcp.c 
/net/ipv4/tcp_input.c 
 /net/ipv4//tcp_output.c   /net/ipv4/tcp_minisocks.c   /net/ipv4/tcp_output.c 
 /net/ipv4/tcp_timer.c etc 
 
.IP层:
处理网络层的操作,网络层用struct packet_type结构表示。 
 文件主要有:/net/ipv4/ip_forward.c   ip_fragment.c   ip_input.c   ip_output.c    etc. 
 
.数据链路层和驱动程序
每个网络设备以struct net_device表示,
通用的处理在 dev.c中, 驱动程序都在/driver/net目录下。 
网络部分还有很多其他文件,如防火墙,路由等,一般根据看到名字便能猜测出相应的处理,此处不再赘述。 
 
现在我要给出一张表,全文的内容就是为了说明这张表(如果你觉得我在文章中的语言比较乏味,尽可抛掉他们,结合这张表自己看代码)。在我最初看网络部分代码时,比较喜欢《linux kernel internals》的第八章的一段,其中有一个进程A通过网络远程向另一进程B发包的例子,详细介绍了一个数据包如何从网络堆栈中走过的过程。我觉得这样可以更迅速的帮助读者看清森林的全貌,因此本文参照这种结构来叙述。 
^ 
| sys_read  fs/read_write.c 
| sock_read  net/socket.c 
| sock_recvmsg  net/socket.c 
| inet_recvmsg  net/ipv4/af_inet.c 
| udp_recvmsg  net/ipv4/udp.c 
| skb_recv_datagram net/core/datagram.c 
| ------------------------------------------- 
| sock_queue_rcv_skb include/net/sock.h 
| udp_queue_rcv_skb net/ipv4/udp.c 
| udp_rcv   net/ipv4/udp.c 
| ip_local_deliver_finish net/ipv4/ip_input.c 
| ip_local_deliver net/ipv4/ip_input.c 
| ip_recv   net/ipv4/ip_input.c 
| net_rx_action  net/dev.c 
| ------------------------------------------- 
| netif_rx  net/dev.c 
| el3_rx   driver/net/3c309.c 
| el3_interrupt  driver/net/3c309.c 
========================== 
| sys_write  fs/read_write.c 
| sock_writev  net/socket.c 
| sock_sendmsg  net/socket.c 
| inet_sendmsg  net/ipv4/af_inet.c 
| udp_sendmsg  net/ipv4/udp.c 
| ip_build_xmit  net/ipv4/ip_output.c 
| output_maybe_reroute net/ipv4/ip_output.c 
| ip_output  net/ipv4/ip_output.c 
| ip_finish_output net/ipv4/ip_output.c 
| dev_queue_xmit  net/dev.c 
| -------------------------------------------- 
| el3_start_xmit  driver/net/3c309.c 
V 
  我们假设的环境如下:有两台主机通过互联网联在一起,其中一台机子运行这一个进程 
A 另外一台运行进程B,进程A将向进程B发出一条信息,比如"Hello",B接受此信息。 
TCP处理本身非常复杂,为了便于叙述,在后面我们将用UDP作为例子。 
 
2.1 建立套接字 
在数据发送之前,要建立一个套接字(socket,在两边的程序中都会调用如下语句: 
... 
int sockfd; 
sockfd=socket(AF_INET,SOCK_DGRAM,0); 
... 
这是个系统调用,因此会通过0x80中断进入系统内核,调用内核中的相应函数.当寻找 系统调用在内核中的对应流程时,一般前面加入"sys_"再找就是了,如对fork来说,就 
 调用sys_fork。但是socket相关调用有些特殊,所有的这类调用都是通过一个入口,即 
sys_socketcall进入系统内核,然后再通过参数调用具体的sys_socket,socket_bind等函数。 
 
  sys_socket会调用sock_create产生一个struct socket结构(见 include/linux/net.h), 每个套接字在内核中都有一个这样的结构对应,在初始化了此结构的一些通用成员后 (如 分配inode,根据第二个参数为type项赋值等),会根据其一个参数作响应的调度,即  一句: 
... 
net_families[family]->create(sock, protocol); 
... 
我们的程序的第一个参数是AF_INET,所以此函数指针会指向inet_create(); net_families 
是个数组,保留了网络协议族(net families)的信息,而这些协议族用sock_register加载。) 
 
struct socket结构结构中最重要的信息保留在struct sock结构中,这个结构在网络代码中经常使用,建议把它和其他常见结构(如struct sk_buff)打印出来放在手边。在 inet_create会为此 结构分配内存,并根据套接字类型(其实就是socket函数的第二个参数),作各自不同的初始化: 
... 
if (sk->prot->init) 
 sk->prot->init(sk); 
... 
如果类型是SOCK_STREAM的话会调用tcp_v4_init_sock,而SOCK_DGRAM类型的socket没有额外的初始化了,到此socket调用结束。 
还有一个值得注意的地方是当inet_create()调用完后,会接着调用sock_map_fd 数,这个函数中会为套接字分配一个文件描述符并分配一个file文件。在应用层便可象处理文件一样 处理套接字了。 
开始的时候可能有些流程难以跟下去,主要便是这些函数指针的实际指向会根据类型变化。 
 
2.2 发送数据 
当进程A想发送数据时,程序中会调用如下语句(如果用send函数的话会走类似的流程,
) 
... 
write(sockfd,"Hello",strlen("Hello")); 
... 
write在内核中对应的函数就是sys_write,此函数首先根据文件描述符找到structfile结构,如果此文件存在(file指针非空)且可写(file->f_mode & FMODE_WRITEtrue),便调用此文件结构的写操作: 
... 
if (file->f_op && (write = file->f_op->write) != NULL) 
 ret = write(file, buf, count, &file->f_pos); 
... 
其中f_op是个struct file_operations结构指针,在sock_map_fd中将其指向socket_file_ops,其定义如下(/net/socket.c): 
static struct file_operations socket_file_ops = { 
llseek:  sock_lseek, 
 read:  sock_read, 
 write:  sock_write, 
 poll:  sock_poll, 
 ioctl:  sock_ioctl, 
 mmap:  sock_mmap, 
 open:  sock_no_open, /* special open code to disallow open via /proc */ 
 release: sock_close, 
 fasync:  sock_fasync, 
 readv:  sock_readv, 
 writev:  sock_writev 
}; 
此时wirte函数指针显然指向了sock_write,我们跟下去看,此函数将一个字符串缓冲整 理成struct msghdr 最后调用了sock_sendmsg. 
sock_sendmsg中的scm_send我不了解(scmSocket level control messages的简 写),好在它也不是很关键, 我们注意到这句: 
... 
sock->ops->sendmsg(sock, msg, size, &scm); 
... 
又是个函数指针,sock->opsinet_create()函数中被初始化,由于我们我们是UDP 的套接字,sock->ops 指向了inet_dgram_ops(即sock->ops = &inet_dgram_ops;),其定义在 net/ipv4/Af_inet.c中: 
struct proto_ops inet_dgram_ops = { 
 family:  PF_INET, 
 release: inet_release, 
 bind:  inet_bind, 
connect: inet_dgram_connect, 
 socketpair: sock_no_socketpair, 
 accept:  sock_no_accept, 
 getname: inet_getname, 
 poll:  datagram_poll, 
 ioctl:  inet_ioctl, 
 listen:  sock_no_listen, 
 shutdown: inet_shutdown, 
 setsockopt: inet_setsockopt, 
 getsockopt: inet_getsockopt, 
 sendmsg: inet_sendmsg, 
 recvmsg: inet_recvmsg, 
 mmap:  sock_no_mmap, 
}; 
因此我们要看得便是inet_sendmsg()函数了,而马上,这个函数又通过函数指针调用 了另一函数: 
... 
sk->prot->sendmsg(sk, msg, size); 
... 
 我们不得不再次寻找其具体指向。看到这里,说点题外话,怎么才能找到其具体定义呢 ?我一般是这样: 
对上例而言,sk是个struct sock结构,到其定义(linux/net/sock.h中)出看到prot 是个struct proto 结构,此时我们便在源代码树中寻找所有此结构的实例(这些诸如跳到定义,寻找引用 等工作在source insight中实在太方便快速了^_^,很快便会发现诸如udp_prottcp_protraw_prot 等,猜测是用了 udp_prot,便再找一下它在源代码中的引用情况,果然发现在inet_create中有这么一句  
... 
prot=&udp_prot; 
... 
其实如果前面看inet_create函数时仔细一点会早点发现了,但我总没有这么细心:) 我们顺着udp_sendmsg往下走;在这个函数的主要作用是填充UDP头(源端口,目的端口等),接着调用了 ip_route_output,作用是查找出去的路由,而后: 
... 
ip_build_xmit(sk,  (sk->no_check == UDP_CSUM_NOXMIT ? udp_getfrag_nosum :udp_getfrag), 
 &ufh, ulen, &ipc, rt, msg->msg_flags); 
... 
ip_build_xmit函数的很大比例是生成sk_buff,并为数据包加入IP头。 后面有这么一句: 
... 
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, 
rt->u.dst.dev,output_maybe_reroute); 
... 
  简单的说,在没有防火墙代码干预的情况下,你可以将此处理解为直接调用output_maybe_reroute 
(具体可参看绿盟月刊14期中的《内核防火墙netfilter入门 》) 
output_maybe_reroute中只有一句: 
return skb->dst->output(skb); 
依旧照上面的方法(不过这个确实不太好找),发现其实这个指针是在 ip_route_output中指定的, 
(提示:ip_route_output_slow中:rth->u.dst.output=ip_output; 
,ip_route_output的作用 便是查找路由,并将结果记录到skb->dst中。 
于是,我们开始看ip_output函数了,而它马上又走向了ip_finish_output~~。 
每个网络设备,如网卡,在内核中由一个net_device表示,在ip_finish_output中找到 其用到的设备 
(也是在ip_route_output中初始化的),这个参数在会传给netfilter 
NF_IP_POST_ROUTING点登记 的函数,结束后调用ip_finish_output2,而这个函数中又会调用: 
... 
hh->hh_output(skb); 
... 
闲话少叙,实际调用了dev_queue_xmit,到此我们完成了TCP/IP层的工作,开始数据链 
路层的处理。 
在做了一些判断之后,实际的调用是这句: 
... 
dev->hard_start_xmit(skb, dev); 
... 
这个函数是在网卡的驱动程序中定义的,每个不同的网卡有不同的处理,我的网卡是比 较通用的3c509 
(其驱动程序是3c509.c),在网卡处理化的时候(el3_probe),有: 
... 
dev->hard_start_xmit = &el3_start_xmit; 
... 
再往下便是IO操作,将数据包真正的发到网络上去,至此发送过程结束。 
中间我说的有些草率,完全没顾的上中间的如出错,阻塞,分片等特殊处理,只是将理 想的过程描述出来。 
这篇短文的目的也只是帮助大家建立个大致的印象,其实每个地方的都有非常复杂的处 理(尤其是TCP部分)。 
 
2.3 接受数据 
当有数据到达网卡的时候,会产生一个硬件中断,然后调用网卡驱动程序中的函数来处 理,对我的3c509网卡来说, 其处理函数为:el3_interrupt。(相应的IRQ号是在系统启动,网卡初始化时通过 request_irq函数决定的。) 
这个中断处理程序首先要做的当然就是进行一些IO操作将数据读入(读IOinw 数),当数据帧成功接受后, 执行el3_rx(dev)进一步处理。 
el3_rx中,收到的数据报会被封装成struct sk_buff,并脱离驱动程序,转到通用的 
处理函数netif_rx dev.c)中。为了CPU的效率,上层的处理函数的将采用软中断的方式激活,netif_rx 的一个重要工作就 是将传入的sk_buff放到等候队列中,并置软中断标志位,然后便可放心返回,等待下 一次网络数据包的到来: 
... 
__skb_queue_tail(&queue->input_pkt_queue,skb); 
__cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ); 
... 
这个地方在2.2内核中一直被称为"底半"处理--bottom half,其内部实现基本类似,目 的是快速的从中断中返回。 
过了一段时间后,一次CPU调度会由于某些原因会发生(如某进程的时间片用完)。在进程调度函数即schedule() 中,会检查有没有软中断发生,若有则运行相应的处理函数: 
... 
if (softirq_active(this_cpu) & softirq_mask(this_cpu)) 
  goto handle_softirq; 
handle_softirq_back: 
... 
... 
handle_softirq: 
 do_softirq(); 
 goto handle_softirq_back; 
... 
  在系统初始化的时候,具体说是在net_dev_init中,此软中断的处理函数被定为 
net_rx_action 
... 
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); 
... 
  当下一次进程调度被执行的时候,系统会检查是否发生NET_TX_SOFTIRQ软中断,若有则 调用net_rx_action 
net_tx_action函数既是2.2版本中的net_bh函数,在内核中有两个全局变量用来登记网络层的, 一个是链表ptype_all,另外一个是数组ptype_base[16],他们记载了所有内核能够处  的第三层(按照OSI7层模型)协议。每个网络层的接收处理由一个 struct packet_type表示,而这个结构将通dev_add_pack函数将他们登记到ptype_all  ptype_base中。只有packet_type中的type项为ETH_P_ALL时,才会登记到ptype_all  中,否则如ip_packet_type,会在数组ptype_base[16]找到相应的位置。两者不同点是 如果是以ETH_P_ALL类型登记,那么处理函数会受到所有类型的包,否则只能处理自己登  的类型的。 
skb->protocol是在el3_rx中赋值的,其实就是以太帧头信息中提取出的上层协议名,  于我们的例子来说,这个值是ETH_P_IP,所以在net_tx_action中,会选择IP层的接收 处理 函数,而从ip_packet_type 不难看出,这个函数便是ip_recv()。 pt_prev->func(实际指向ip_recv)前面有一个atomic_inc(&skb->users)操作(在2.2 内核中这个地方是一句skb_clone,原理类似),目的是增加这个sk_buff的引用数。网 络层 的接收函数在处理完或因为某些原因要丢弃此sk_buff时(如防火墙)会调用 kfree_skb kfree_skb中首先会检查是否还有其他地方需要此函数,如果没有地方再用,才真正 释放 此内存(__kfree_skb),否则只是计数器减一。 
 
现在我们便来看看ip_recvnet/ipv4/ip_input.c)。这个函数的操作是非常清晰的: 首先检查这个包的合法性(版本号,长度,校验和等是否正确),如果合法则进行接下  的处理。在2.4内核中,为了灵活处理防火墙代码,将原来的一个ip_recv分成了两部 分, 即将将原来的的ip_recv的后半段独立出一个ip_rcv_finish函数。在ip_rcv_finish 中, 一部分是带有IP选项(如源路由等)的IP包,例外就是通过ip_route_input查找路由, 并将结果记录到skb->dst中。此时接收到的包有两种,发往本地进程(需要传往上层协议) 或转发(用作网关时),此时需要的处理函数也不相同,如果传往本地,则调用 ip_local_deliver (/net/ipv4/ip_input.c),否则调用 ip_forward(/net/ipv4/ip_forward.c).skb->dst->input 这个函数指针会将数据报领上正确的道路。 
对我们的例子而言,此时应该是调用ip_local_deliver的时候了。 发来的包很有可能是碎片包,这样的话则首先应该把它们组装好再传给上层协议,这当 然也是 ip_local_deliver函数所做的第一份工作,如果组装成功(返回的sk_buff不为空), 则继续处 理(详细的组装算法可参见绿盟月刊13期中的《IP分片重组的分析和常见碎片攻 击》)。 此时代码又被netfilter一分为二了,象前面一样,我们直接到后半段,即 ip_local_deliver_finish (/net/ipv4/ip_input.c)中去。   
传输层(如TCP,UDPRAW)的处理被登记到了inet_protos中(通过 inet_add_protocol)。 ip_local_deliver_finish会根据 IP头信息中的上层协议信息(即iph->protocol),调用相应的处理函数。为了简便, 我们 采用了udp,此时的ipprot->handler实际便是udp_rcv了。 
前面已经提到,在应用程序中建立的每个socket在内核中有一个struct socket/struct sock 对应。udp_rcv会通过udp_v4_lookup首先找到在内核中的sock,然后将其作参数调用 udp_queue_rcv_skb/net/ipv4/udp.c)。马上,sock_queue_rcv_skb函数被调用, 此函数将sk_buff放入等待队列,然后通知上层数据到达:
 ... 
kb_set_owner_r(skb, sk); 
skb_queue_tail(&sk->receive_queue, skb); 
if (!sk->dead) 
 sk->data_ready(sk,skb->len); 
return 0; 
... 
 
sk->data_ready的定义在sock结构初始化的时候(sock_init_data): 
... 
sk->data_ready=sock_def_readable; 
... 
 
现在我们便要从上往下看起了: 
进程B要接收数据报,在程序里调用: 
... 
read(sockfd,buff,sizeof(buff)); 
... 
此系统调用在内核中的函数是sys_read(fs/read_write.c)以下的处理类似write的操 作, 
不再详述.udp_recvmsg函数会调用skb_recv_datagram,如果数据还没有到达,且 socket 设为阻塞模式时,进程会挂起(signal_pending(current)),直到data_ready通知进程 资源得到满足后继续处理(wake_up_interruptible(sk->sleep);)。 
 
 
2.4 skbuff 
网络代码中有大量的处理涉及对sk_buff的操作,尽管此文中尽量将其回避了,但在仔 细分析的时候 则必须对此作分析,数据包在网络协议层是以sk_buff的形式传送处理的,可以说它是网络部分最重要的数据结构。具体分析建议参看alan cox的《Network Buffers And Memory Management》,这片发表在199610月的linux journal上。  这里引用phrack 55-12期中的一幅图,尽管它只描绘了sk_buff的极小的一个侧面,但却非常有用,尤其是当你像我一样总忘记了skb_put是向前还是向后调指针的时候:) 
 
    --- -----------------hand 
     ^ |  | 
     | |  |   ^ skb_push 
     | |  |   | 
 
     | -----------------data--- --- 
     | |  |     ^   | 
   true |  |     |   v skb_pull 
   size |  |    len 
     | |  |     |   ^ skb_trim 
     | |  |     v   | 
 
     | -----------------tail--- --- 
     | |  |   | 
     | |  |   v  skb_put 
     v |  | 
 
--- -----------------end 
 
linux网络层效率:
linux的网络层代码中指针被大量应用,其目的就是避免数据拷贝 这类耗费系统资源的操作。 一个数据包的数据段部分在读入或发出时只经过两次拷贝,即从网卡中考到核心态内存,和从核心态内存考到用户态内存。前些天看到,在一些提高sniffer抓包效率的尝试中,turbo packet(一个内核补丁)采用了核心态和用户态共享一段内存的办法,又减少了一次数据拷贝,进一步提高了效率。 
 
3 后记 
这次的投稿又是到了最后关头仓促写出来的,看着里面拙劣的文笔,实在觉得有点对不 住观众~~如果有时间 我会把这部分好好重写的,其实这也是我一直的愿望:) 
 
  4 参考文献: 
[1.] phrack 55-12 
[2.]  2nd Edition 
[3.] Network Buffers And Memory Management  Alan Cox 
http://www2.linuxjournal.com/lj-issues/issue30/1312.html
 [4.] 浙大源码分析报告《Linux网络设备分析》潘纲 
 [5.] Linux IP Networking--A Guide to the Implementation and Modification of theLinux Poptocol Stack 

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/22214587/viewspace-709782/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/22214587/viewspace-709782/

你可能感兴趣的:(网络,数据结构与算法)