Linux与FreeBSD中TCP协议栈实现之比较

wenjieshen 2011-11-08

作为两个最有名的开源操作系统,Linux和FreeBSD是网管们的首选。Linux以开放性和众多的驱动支持著称,而FreeBSD有着优良的UNIX传统,是公认的最稳定的操作系统。那么,在这两个操作系统间,该如何选择呢?幸好,我们有源码,可以从协议栈的实现中寻找答案。

TCP/IP协议栈是网络中广泛使用的事实网络通信标准。最初的TCP实现源自4.4BSDlite,在Linux兴起后,也不可避免得支持它。但Linux的实现自成体系,仅与传统实现保持接口上的兼容,下面我们将针对源码级的实现,来分析一下两者的异同。但是,对于Linux和FreeBSD这样优秀的系统来说,已经无所谓何优何劣,有的仅仅是实现策略与侧重点上的不同而已。

从进程的角度上讲,可以调用send,sendto,sendmsg来发送一段数据,来可以使用文件系统中的write和writev来发送数据。同理,接收数据可以使用相应的recv,recvmsg,recvfrom,也可以使用文件系统提供的read,readv来接收一段数据。对于接收来说,这是异步进行的,也就是说,这是中断驱动的,在以后的分析中,我们要注意这点。为简单起见,同时不失一般性,我们将分析TCP协议的输入输出全过程,并以已对LINUX及FreeBSD的实现作一对比。

首先我们来看FreeBSD上的协议实现,这也是最正统的实现。下面是完整的输入输出路径。

Linux与FreeBSD中TCP协议栈实现之比较

首先来看左边的输出,不管应用程序调用哪个输出函数,最终都要调用sosend来完成输出。Sosend将从用户空间把数据复制进内核管理的m_buf数据结构,m_buf是FreeBSD的TCP实现使用的数据缓冲结构。在sosend完成数据复制后,将调用TCP的输出函数,tcp_output要做的事情是分配一个新的m_buf来保存tcp头,并计算相应的数据校验码,在下一步的ip_output中,同样也要进行数据校验工作,并进行数据路由选择。最终ether_output将通过if_start来调用具体的硬件驱动程序来完成数据发送。在某个网卡的驱动中,ex_start将负责将数据从内核的m_buf缓冲复制进硬件自己的缓冲区,以完成数据发送工作。在这整个过程中,数据被复制两次,并且也被遍历两次(计算校验码),这也是主要的影响效率的地方。

再来讨论右边的输入,当网卡收到数据时,中断处理程序ex_intr将被调用。驱动通过ex_rx_intr将数据从硬件缓冲复制进m_buf数据结构中,并调用ether_input来进一步处理。ether_input通过ether_demux进行分用。如果是一个Ip包,将通过软中断调用ip_fastforward进行数据校验,并判断是否要转发,如果失败,将进行ip_input进行完整的处理。在in_input中,同样要判断是否要进行转发,如果不用,调用tcp_input进行进一步处理。在tcp_input中,进行数据校验和验证后,有一个叫做首部预测算法的优化,可以加快数据处理速度。进行完所有的操作后,如果是用户数据,将唤醒用户进程进行处理。同理,用户可以使用多个函数进行数据接收,而soreceive将负责将数据从m_buf转移至用户进程缓冲。

可以看出,在FreeBSD中,发送和接收数据,所进行的操作差不多,都要进行两次数据复制和两次数据遍历,这也是最大的影响效率的地方。两次数据复制似乎无可避免,下面我们来看看Linux是怎么做的。

Linux与FreeBSD中TCP协议栈实现之比较

可以看到,在LINUX上的实现稍显复杂。让我们首先从发送开始分析。在LINUX上,socket被实现为一个文件系统,这样可以通过vfs的write来调用,也可以直接使用send来调用,它们最终都是调用sock_sendmsg。Sock_sendmsg通过它的内核版本__sock_sendmsg直接调用tcp_sendmsg来发送数据。在tcp_sendmsg中,同时完成数据复杂和数据校验,这样节省了一次遍历操作,这也是和FreeBSD不同的地方。Linux使用skb结构来管理数据缓冲,这和FreeBSD的m_buf大同小异。当复制完数据后,使用tcp_push来进行下一步发送。Tcp_push通过__tcp_push_pending_frames来调用tcp_write_xmit将数据填入tcp的发送缓冲区。这里的填充仅是指针引用而已。下一步,tcp_transmit_skb将数据放入IP的发送队列。Ip_queue_xmit函数完成IP包头的设置以及数据效验,并调用ip_output进入下一步发送。如果不用分片,将使用ip_finish_output继续发送。在这里,填充数据的以太网包头后调用dev_queue_xmit函数来进一步处理。Dev_queue_xmit函数将数据转移至网络核心层的待发送队列,调用具体的驱动程序cp_start_xmit来完成数据的最终发送。最后的cp_start_xmit做的事情和freebsd的相应函数差不多,检查数据,并复制进硬件缓冲。

当接收到一个数据包的时候,网卡会产生中断,这样网卡驱动的cp_interrupt会被调用。cp_interrupt做的事情很少,只进行必要的检查后就返回了,更多的事情通过cp_rx_poll来完成,cp_rx_poll在软中断中被调用,这样做是为了提高驱动的处理效率。Cp_rx_poll做的事情主要就是把申请并将数据复制进一个skb缓冲中。netif_rx函数将数据从这个队列中转移至网络核心层队列中,netif_receive_skb从这里接收数据,调用ip_rcv来处理。Ip_rc和ip_rcv_finish一起检查数据,得到包的路由,并调用相应的input函数来完成路由,在这里就是ip_local_deliver,ip_local_deliver完成IP包的重组后,使用ip_local_deliver_finish来进入tcp的处理流程,tcp_v4_rcv完成数据校验以及一些简单的检查,主要的工作在tcp_v4_do_rcv中完成。tcp_v4_do_rcv先判断是否正常的用户数据,如果是则用tcp_rcv_established处理,否则用tcp_rcv_state_process来更新连接的状态机。tcp_rcv_established中同样有首部预测。如果一切顺便,将唤醒等待在tcp_recvmsg中的用户进程。后者将数据从skb缓冲中复制进用户进程缓冲。并进行逐级返回。

通过以上分析不难看出,Linux的代码比较混乱,可读性没有FreeBSD的好,比如说,Linux省略了以太网层,而且在接收数据中有多次异步操作,也许这将会影响内核的稳定性,FreeBSD的代码就比较清晰,程序处理一目了然,可读性也高,最稳定的操作系统名不虚传。这也可以从两个操作系统的起源得到解释。Linux起源于互联网时代,由众多爱好者一起完成,并没有一个完整的规划,代码也多次经过变动,而作者水平也参次不齐,造成现在的样子。而FreeBSD系出名门,一直由一个独立的小组进行维护,多年来更新不大,只有少许优化,所以代码的可读性非常高。但从另一方面讲,不断更新的Linux在代码方面比较激进,比如Linux使用skb缓冲效率要较FreeBSD使用的m_buf为高,这里限于篇幅,就不再具体分析了。而且linux发送数据时,在复制数据的同时完成的tcp的效验,这样就节省了一次数据的遍历操作。也提高了效率。

相关推荐