linux内核网络协议栈

未完,待续...

Raw_Socket原始套接字

ARP的C代码实现

3 AF_PACKET发送以太网帧

4 ARP发送

5 send arp

6 connection reset by peer

3.4.1 SKB的缓存池

connection reset by peer:连接被对方重设 是服务器向客户传输数据时由于超负荷、网络中断、防火墙影响或未按规定关闭网络时导致的问题。 出现该错误,重启即可。要避免该错误,需要在程序退出前关闭所有网络。 netstat -an 查看连接数.

[socket]

一创建

 socket(int family, int type, int protocol);

1    family:AF_INET,AF_PACKET; 

    协议族:创建socket时使用.

    每个协议族都通过packet_init/inet_init->sock_register函数将(packet_familiy_ops/inet_familiy_ops)

    注册给net_families[];

    socket->__sock_create创建时,通过pf->create找到对应的inet_familiy_ops->inet_create或packet_create等.

   socket关闭:struct linger 用法

 2   type=SOCK_STREAM;SOCK_DGRAM;SOCK_RAW;SOCK_PACKET;

   socket类型:创建socket时使用;

    如:inet_create中通过sock->type(__sock_create中将sock->type=type),遍历inetsw链表;

           通过inetsw链表找到inetsw_array[sock_type],所以也相当于遍历了inetsw_array[];

  查找inetsw_array[sock_type]是通过遍历inetsw_array[],然后比较每个inetsw_array[].protocol与socket调用传人的protocol是否相同.

            inet_init时注册:inet_register_protosw(inetsw_array[]);此时inetsw_array[]和inetsw关联;

            inet_create中根据inetsw_array[type]初始化sock->ops=inetsw_array[type].ops;

struct inet_protosw inetsw_array[]=

{

            {

.type=SOCK_STREAM,

.protocol=IPPROTO_TCP,

.proto = &tcp_prot,

.ops = inet_stream_ops,

             },

}

struct proto_ops inet_stream_ops = {

.recvmsg=inet_recvmsg, //调用sk->sk_prot->recvmsg=tcp_recvmsg

}

inet_recvmsg->tcp_recvmsg

struct proto tcp_prot={

.recvmsg=tcp_recvmsg,

}

recvmsg->__sock_recvmsg->(sock->ops->recvmsg())此时的sock->ops是在inet_create中初始化的inetsw_array[type].ops.

3 packet:IPPROTO_IP/IPPROTO_TCP等;

     socket->__sock_create->inet_create时根据inetsw找到对应inetsw_array[]

2 socket->sock_create->__sock_create

二  socket设置

1 设置接口:sock_setsockopt();

sock->sk_rcvbuf=min(val,sysctl_rmem_max);

2 如果未设置,则默认初始为:

sock_init_data(): 

sock->sk_rcvbuf=sysctl_rmem_default;

sock->sk_sndbuf=sysctl_wmem_default;

3 如果四层协议为tcp,则会再次初始化:

inet_create()

{

    sock_init_data();

    if(sk->sk_prot->init)

          sk->sk_prot->init(sk);

}

对于tcp:sk_prot->init=tcp_v4_init_sock()

tcp_v4_init_sock()->tcp_init_sock();

sock->sk_rcvbuf= sysctl_tcp_rmem[1];

sock->sk_sndbuf=sysctl_tcp_wmem[1];

4 tcp建立连接过程,tcp_rcv_state_process有可能通过

tcp_init_buffer_space->tcp_fixup_rcvbuf(sk);修改sk_rcvbuf;

5 tcp收包过程中:

tcp_v4_do_rcv->tcp_rcv_established->tcp_data_queue()->tcp_clamp_window

收包过程tcp_clamp_window减少接收窗口过程,根据接收窗口调整socket buf大小,

但socket buf大小不能超过sysctl_tcp_rmem[2];

tcp_recvmsg通过tcp_rcv_space_adjust动态调整sk_rcvbuf;

tcp_sendmsg->sk_wmem_schedule /

tcp_sendmsg->sk_stream_alloc_skb->sk_wmem_schedule :判断是否超过发送socket buf

sk_rmem_schedule

6  skb入队列 :

__skb_queue_tail(&sk->sk_receive_queue,skb);

tcp_v4_do_rcv->tcp_rcv_established->tcp_queue_rcv

【sk_buff】

1 申请与释放:

主要在驱动中使用的接口:

netdev_alloc_skb()->__alloc_skb(); 

dev_kfree_skb->__free_skb(); dev_kfree_skb中判断skb->users

1 网络驱动初始化过程:确定接收和发送包的描述符个数,该描述符个数可以通过dts文件指定;

2 驱动open时,会根据1中指定的描述符个数申请接收和发送包的描述符;

3 每个描述符结构中有一个sk_buff变量,即skb。

1>  接收包过程会在申请每个网络包描述符的过程中,同时申请skb和skb->data;并且通过netif_receive_skb()将skb传入协议栈进行处理;

2> 发送包的过程申请每个包描述符时,不申请对应的skb,用户发送数据时在协议栈申请skb,并且传入驱动中的*xmit类函数,该函数中将skb,赋值给包描述符,然后进行中断发送,并且中断处理在发送完成后会通过dev_free_skb_irq()触发软中断,NET_TX_SOFTIRQ;该软中断处理net_tx_action->__kfree_skb中释放skb;

skb的释放:skb在协议栈中释放. kfree_skb时分别将skb释放给kmem_cache_free(skbuff_head_cache);且skb->data通过kfree释放.

1 驱动认为在netif_receive_skb/netif_rx过程如果处理错误,则协议栈已经释放了skb;

2 netif_receive_skb/netif_rx如果处理正确,tcp_v4_do_rcv将skb挂载到了收包队列,等待用户通过tcp_recvmsg,用户接收完成后,再释放skb;

tcp_recvmsg :如果用户设置了MSG_PEEK标志,则用户收完包后,不释放skb;如果没有设置该标记,则通过tcp_recvmsg->sk_eat_skb释放skb;

协议栈中正常使用接口:

alloc_skb()->__alloc_skb();

kfree_skb->__free_skb();  kfree_skb种判断skb->users

tcp_sendmsg->sk_stream_alloc_skb->alloc_skb_fclone->__alloc_skb();

socket buf 使用:

tcp_v4_rcv->__inet_lookup_skb()中根据接收到的skb中保存的源、目的端口找到对应的接收socket,如果没有socket

用于接收该skb,则discard即kfree_skb(); inet_hash_connect时创建关系;

[收包]

网络包接收分两大部分:

第一:mac驱动层收包,软中断将包放在sock的接收队列中:sock->sk_receive_queue;然后tcp_v4_do_rcv唤醒用户进程收包。

用户进程在tcp_recvmsg->sk_wait_data中等待;

第二:用户进程通过recv->tcp_rcvmsg收包,然后等待sk_receive_queue队列上有skb到达;

1 驱动->协议栈:netif_receive_skb();

驱动层收包会:skb->protocol=eth_type_trans()处理,得到上层协议类型,如:ETH_P_IP/ETH_P_8021Q;

skb->protocol会影响网络层的处理方式(ip_rcv/arp_rcv);

2 协议栈逐层传递:

 __netif_recive_skb_core->deliver_skb()

 遍历ptype_all;参考skb->protocol调用packet_type->func();不同的协议族对应的packet如ip_packet_type,有各自不同的sock,

 每个sock都有sock->sk_receive_queue接受队列;

ptype_all通过dev_add_pack注册,

packet_rcv,ip_rcv、arp_rcv的选取和skb->protocol有关,ETH_P_IP对应ip_rcv;ETH_P_8021Q对应arp_rcv();

如ip_rcv注册:inet_init->dev_add_pack(&ip_packet_type);//ip_packet_type>func=ip_rcv;

arp_rcv注册:arp_init->dev_add_pack(&arp_packet_type);

packet_rcv注册:packet_bind/packet_create->register_prot_hook->dev_add_pack(struct packet_type);

netif_receive_skb时调用,和ip_rcv同级.

packet_rcv中将skb挂载到接收队列(sock->sk_receive_queue每个socket对应一个接收队列)

sock=packet_type->af_packet_priv上,并唤醒用户接收数据包;

3 ip层传递:ip_rcv->ip_rcv_finish->dst_input=ip_local_deliver->inet_protos[protocol]->handler();

inet_protos[]在inet_init中通过inet_add_protocol(&tcp_protocol)注册.

ip层继续向上传递到四层协议处理;

arp传递:arp_rcv 直接处理arp协议,不向上层传递;

packet_rcv:直接在packet_rcv中唤醒用户接收,不向上传递,sk->sk_data_ready=sock_def_readable;

AF_PACKET接收报文,未去mac协议头;

4 tcp层传递:

三层向四层传递过程调用了:inet_protos[protocol]->handler();

而inet_protos[]是在inet_init中通过inet_add_protocol(&tcp_protocol)注册.如:udp_protocol/icmp_protocol等.

tcp层收包:tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established->tcp_queue_rcv->__skb_queue_tail(&sk->sk_receive_queue,skb);

static struct tcp_protocol tcp_protocol={

.handler = tcp_v4_rcv,

}

static net_protocol udp_protocol = {

.handler = udp_rcv,

}

在Linux内核的TCP实现中,TCP有三个接收队列——除去错误队列。这三个队列分别是struck sock中的sk_receive_queue和sk_backlog,以及struct tcp_sock中的prequeue。

TCP中的三个接收队列

TCP的三个接收队列

TCP的输入

TCP prequeue

评价linux协议栈tcp实现中的prequeue

5 用户收包tcp_recvmsg:

int tcp_recv_msg()

{

/*

在Linux内核的TCP实现中,TCP有三个接收队列——除去错误队列。这三个队列分别是struck sock中的sk_receive_queue和sk_backlog,以及struct tcp_sock中的prequeue。这三个队列作用。这里只简单介绍一下,sk_receive_queue是真正的接收队列,收到的TCP数据包经过检查和处理后,就会保存到这个队列中。sk_backlog是当socket处于用户进程的上下文时(即用户正在对socket进行系统调用,如recv),Linux收到数据包时,在软中断处理过程中,会将数据包保存到sk_backlog中,然后直接返回。而prequeue则是在,该socket没有正在被用户进程使用时,由软中断直接将数据包保存在prequeue中,然后返回。

release_sock->sock_release_ownership(sk)时解锁

tcp_v4_rcv函数据此判断skb放入prequeue队列,还是backlog,若锁定则选择prequeue

*/

    lock_sock();

    do(

            //如果开启低延迟或没有处于用户接收进程上下文时,不放入prequeue队列

            if(!sysctl_tcp_low_latency&&tp->ucopy.task==user_recv)

            {

                if(!user_recv&&!(flags&(MSG_TRUNC|MSG_PEEK)))

                {

                        user_recv=current;

                        tp->ucopy.task=user_recv;

                }

            }

            if(copied >=target)

            {

                release_sock(sk);//接收backlog队列上的skb,->sock_release_ownership表示软中断收包放入prequeue队列

            //sock_owned_by_user(sk)=sk->sk_lock.owned=1,软中断收包会将skb放在backlog:sk_add_backlog()

            //sk->sk_lock.owned=0时,软中断收包通过tcp_prequeue将skb放入prequeue队列

                lock_sock(); 

            }else

               sk_wait_data(sk,&timeo); //tcp_prequeue中唤醒

    )while(len>0);//len是用户recvmsg传入的长度

    if(user_recv)

    {

        tp->ucopy.task=NULL;

    }

}

6 软中断收包tcp_v4_rcv

在Linux内核的TCP实现中,TCP有三个接收队列——除去错误队列。这三个队列分别是struck sock中的sk_receive_queue和sk_backlog,以及struct tcp_sock中的prequeue。这三个队列作用。这里只简单介绍一下,sk_receive_queue是真正的接收队列,收到的TCP数据包经过检查和处理后,就会保存到这个队列中。sk_backlog是当socket处于用户进程的上下文时(即用户正在对socket进行系统调用,如recv),Linux收到数据包时,在软中断处理过程中,会将数据包保存到sk_backlog中,然后直接返回。而prequeue则是在,该socket没有正在被用户进程使用时,由软中断直接将数据包保存在prequeue中,然后返回。
 
我们可以从tcp的接收处理函数中,验证上面的结果。下面的代码来自tcp_v4_rcv函数
    bh_lock_sock_nested(sk);

    ret = 0;

    /*没有用户在收包:tcp_recvmsg->locksock(),入口即锁定sock,即if为真表示没有用户进程调用recvmsg类函数*/

    if (!sock_owned_by_user(sk)) {
       if (!tcp_prequeue(sk, skb))
           ret = tcp_v4_do_rcv(sk, skb);
    } else if (unlikely(sk_add_backlog(sk, skb))) {
         
        bh_unlock_sock(sk);
        NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
        goto discard_and_relse;
    }
    bh_unlock_sock(sk);

backlog和prequeue都是保存的未经处理的数据,为什么需要两个不同的队列呢?为了解答这个疑问,我们需要研究一下prequeue和backlog是如何应用的?前面是两个队列的写入操作,下面看看两个队列何时被读取。prequeue的处理函数tcp_prequeue_process,如前文所说,在TCP的读取数据函数tcp_recvmsg中调用。在tcp_recvmsg的入口,会调用lock_sock来设置sk->sk_lock.owned,表示该sock由用户进程占有,然后会对receive_queue和prequeue中的数据包进行处理。正因为sock被用户进程处理时,会访问prequeu,所以软中断只能将数据保存到backlog中,以避免竞争。那么为什么在sock不由用户进程占有时,只能保存到prequeu中,而不能重入backlog呢?

让我们继续跟进,看看何时处理backlog的数据包。Oh,my god,居然是在__release_sock中,这真的有点出乎我的意料。这也就解释了,为什么需要两个队列来保存未处理数据包。对于sock来说,一共有两种状态:1. 用户进程占用该sock;2. 用户进程未占用该sock;而kernel需要在任何情况下,都要能够保证tcp数据包处理的软中断快速返回。而保存未处理数据包的队列,无论如何也要在上述的一个情况下,访问未处理的数据包。那么这不可避免的会有资源竞争。所以为了避免这种情况,当sock被用户进程占用时,让它处理prequeue中的数据包,软中断则往backlog中保存。当sock不被用户进程占用时,会去访问backlog中的数据包,软中断则往prequeue中保存。

tcp_prequeue_process函数会将prequeue中的skb放入tcp_v4_do_rcv函数中:

7 tcp收包总结:

     在启用tcp_low_latency时,TCP传输控制块在软中断中接收并处理TCP段,然后将其插入到sk_receive_queue队列中,等待用户进程从接收队列中获取TCP段后复制到用户空间中,最终删除并释放。
    不启用tcp_low_latency时,能够提高TCP/IP协议栈的吞吐量及反应速度,TCP传输控制块在软中断中将TCP段添加到prequeue队列中,然后立即处理prequeue队列中的段,如果用户进程正在读取数据,则可以直接复制数据到用户空间的缓冲区中,否则添加到sk_receive_queue队列中,然后从软中断中返回。在多数情况下有机会处理prequeue队列中的段,但只有当用户进程在进行recv类系统调用返回前,才在软中断中复制数据到用户空间的缓冲区中。
     在用户进程因操作传输控制块而将其锁定时,无论是否启用tcp_low_latency,都会将未处理的TCP段添加到后备队列中,一旦用户进程解锁传输控制块,就会立即处理后备队列,将TCP段处理之后添加到sk_receive_queue队列中。

1)驱动收包,软中断net_rx_action或netif_receive_skb将skb传到tcp层

2)tcp_v4_rcv处理skb,没有用户recv收包时,tcp_v4_rcv->tcp_prequeue将skb放入prequeue队列上。

TCP收到skb后调用tcp_v4_do_rcv函数进行处理之前会先调用tcp_prequeue函数,将skb放入prequeue队列.如果放入失败,则直接放入sk_receive_queue队列上.

有用户收包时,放入backlog.

3) tcp_recvmsg用户收包时,tcp_recvmsg->tcp_prequeue_process函数会将prequeue中的skb放入tcp_v4_do_rcv函数中.

如果用户进程正在读取数据,tcp_v4_do_rcv->tcp_rcv_established 可以直接复制数据到用户空间的缓冲区中,否则添加到sk_receive_queue队列中将skb放入sk_receive_queue中处理.

prequeue的积累性:prequeue将skb积累于一个队列,这种积累时间长了会带来延迟,因此它只在ucopy拥有进程上下文的时候才进行skb的积累,最小化延迟,最终prequeue的积累最小化了进程切换,否则如果没有prequeue的话,将尽可能往receive_queue中放置skb,然后每放置一个就会wakeup那个进程,这可能会导致进程频繁切换

Linux下的socket编程实践(六)Unix域协议和socketpair传递文件描述符

setsockopt用法详解

关于TCP_NODELAY 和 TCP_CORK选项

TCP_NODELAY 和 TCP_CORK主要区别;

tcp_nodelay:禁止nagle算法,有需要发送的就立即发送,比较常见;

Nagle算法用于对缓冲区内的一定数量的消息进行自动连接。该处理过程(称为Nagling),通过减少必须发送的封包的数量,提高了网络应用 程序系统的效率。(Nagle虽然解决了小封包问题,但也导致了较高的不可预测的延迟,同时降低了吞吐量。)
vc下面socket编程,使用阻塞方式的时候,会自动使用Nagle算法,如:当pc不断发送32Bytes的数据的时候,会将这些包合并起来一起发送。如果另一头使用的时候一个tcp包一条命令处理的话,会出问题的。

tcp_cork:它是一种加强的nagle算法,过程和nagle算法类似,都是累计数据然后发送。但它没有 nagle中1的限制,所以,在设置cork后,即使所有ack都已经收到,但我还是不想发送数据,我还想继续等待应用层更多的数据,所以它的效果比nagle更好。效率上与Nagle算法相比,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小;
tcp_cork使用方法:
int on = 1; 
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* 设置cork */ 
write (fd, …); 
...
sendfile (fd, …); 
… 
on = 0; 

setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on));   /* 拔去塞子 ,发送数据*/ 

TCP_QUICKACK 
阻止因发送无用包而引发延迟的另一个方法是使用TCP_QUICKACK选项。这一选项与 TCP_DEFER_ACCEPT不同,它不但能用作管理连接建立过程而且在正常数据传输过程期间也可以使用。另外,它能在客户/服务器连接的任何一方设 置。如果知道数据不久即将发送,那么推迟ACK包的发送就会派上用场,而且最好在那个携带数据的数据包上设置ACK 标志以便把网络负载减到最小。当发送方肯定数据将被立即发送(多个包)时,TCP_QUICKACK选项可以设置为0。对处于“连接”状态下的套接字该选 项的缺省值是1,首次使用以后内核将把该选项立即复位为1(这是个一次性的选项)。 
在某些情形下,发出ACK包则非常有用。ACK包将确认数据块的接收,而且,当下一块被处理时不至于引入延迟。这种数据传输模式对交互过程是相当典型的,因为此类情况下用户的输入时刻无法预测。在Linux系统上这就是缺省的套接字行为。 
在上述情况下,客户程序在向服务器发送HTTP请求,而预先就知道请求包很短所以在连接建立之后就应该立即发送,这可谓HTTP的典型工作方式。既然没有 必要发送一个纯粹的ACK包,所以设置TCP_QUICKACK为0以提高性能是完全可能的。在服务器方,这两种选项都只能在侦听套接字上设置一次。所有 的套接字,也就是被接受呼叫间接创建的套接字则会继承原有套接字的所有选项。 

通过TCP_CORK、TCP_DEFER_ACCEPT和TCP_QUICKACK选项的组合,参与每一HTTP交互的数据包数量将被降低到最小的可接 受水平(根据TCP协议的要求和安全方面的考虑)。结果不仅是获得更快的数据传输和请求处理速度而且还使客户/服务器双向延迟实现了最小化。

nagle算法相关内核代码:

tcp_write_xmit->tcp_nagle_test

tcp.c/do_tcp_setsockopt():TCP_NODELAY

延迟ack定时器: tcp_delack_timer_handler();

tcp窗口:

抓包时显示的窗口:tcphdr->window 

ack时窗口赋值:tcp_make_synack {th->window=htons(min(req->rcv_wnd,65535U));}

正常发包时加window:

tcp_transmit_skb()

{

    if(tcp->tcp_flags&TCPHDR_SYN)

        th->window=htons(min(th->rcv_wnd,65535));

    else

         th->window = htons(tcp_select_window(sk));

}

几个特殊TCP报文及TCP

TCP segment of a reassembled PDU 说明

wireshark抓包的时候,会出现如下的内容[TCP segment of a reassembled PDU],说明发送端发送的TCP缓存数据过大,需要进行分片发包,分片发包过程中,发送端发送的数据报文中的Ack(Acknowledgment number)编号保持一致 详细 TCP在...

场景
当wireshark抓包的时候,会出现如下的内容[TCP segment of a reassembled PDU],说明发送端发送的TCP缓存数据过大,需要进行分片发包,分片发包过程中,发送端发送的数据报文中的Ack(Acknowledgment number)编号保持一致

详细
TCP在发起连接的第一个报文TCP头里面通过MSS(Maximum Segment Size),告知对方本端能够接收的最大报文(TCP净荷的大小),TCP层收到上层大块报文后,会分解成小块报文发送

猜你喜欢

转载自blog.csdn.net/eleven_xiy/article/details/78016011