协议栈的部分笔记

时间:2022-08-31 04:18:16

ARP条目的类型:

/* ARP Flag values. */
#define ATF_COM 0x02 /* completed entry (ha valid) */
#define ATF_PERM 0x04 /* permanent entry */
#define ATF_PUBL 0x08 /* publish entry */
#define ATF_USETRAILERS 0x10 /* has requested trailers */
#define ATF_NETMASK 0x20 /* want to use a netmask (only
                       for proxy entries) */
#define ATF_DONTPUB 0x40 /* don't answer this addresses */

什么时候会发送ARP查询,目的IP和你网卡知道的IP在同一网段的才会发送ARP请求,比如,网卡IP为192.168.1.102,那ping 192.168.1.0的IP会发送ARP请求MAC地址。如果网卡IP为192.168.1.102,网关设置为192.168.10.1,那ping 192.168.1.0和192.168.10.0两个网段才会发送ARP请求,ping其他网段的IP都直接发送ICMP包。
所以我们说ARP是邻居协议,不存在被转发的情况。
————
struct sk_buff是socket buffer结构,
sk_buff的data和tail指向协议数据区域的起始和结束位置,head和end指向数据在内存中的起始和结束位置。
skb_realloc_headroom(struct sk_buff *skb, unsigned int headroom)函数用于在skb的head和data之间多分配点儿空间, data的内容整体后移,headroom是多分配的长度。。

ip_route_input()负责选择路由:

ip_route_input()
    -> ip_route_input_slow()
        if (res.type == RTN_LOCAL) //本地
            rth->u.dst.input= ip_local_deliver;
        else //转发
            ip_mkroute_input()
                -> __mkroute_input()
                    rth->u.dst.input = ip_forward;
                    rth->u.dst.output = ip_output;
                    skb_dst_set(skb, &rth->u.dst); //skb->_skb_dst = (unsigned long)&rth->u.dst;

如果是发到ip_local_deliver(),如果数据包是分片包,该函数先通过ip_defrag()重组IP分片(因为发往本地就是数据包到终点了,所以要重组分片),然后执行NF_INET_LOCAL_IN上的hook函数,接着调用ip_local_deliver_finish(),该函数将数据包的IP头去掉,然后根据IP头的协议类型从全局数组inet_protos[protocol]中找到对应的L4层协议,并执行其handler方法,如udp_rcv()和tcp_v4_rcv()。
在接收到ICMP错误信息(如ICMP_DEST_UNREACH/ICMP_TIME_EXCEED/ICMP_QUENCH)时,会交给ICMP注册的handler方法icmp_unreach(),然后icmp_unreach()在ICMP数据中找到应该交给UDP还是TCP还是RAW,然后调用相应协议的err_handler方法。

如果是需要转发,由于ip_route_input()已经把路由找到了并保存到skb的dst字段中,所以,ip_forward()的工作就轻松了许多。ip_forward()做的事情:
1. ip_hdr(skb)->ttl <= 1则丢弃;
2. 是否设置了严格路由:
rt = skb_rtable(skb);
if (opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto sr_failed;
3. TTL减1:ip_decrease_ttl(iph);
4. 进入NF_INET_FORWARD的netfilter进程处理。
5. ip_forward_finish()
-> 有option则ip_forward_options(skb)处理IP选项。
-> dst_output(skb)交给上面注册的ip_output()函数。

ip_output()的工作就是找到应该从哪个设备发出去:
1. 将skb->dev赋值为路由过程中找到的目标设备skb_dst(skb)->dev。
2. skb->protocol = htons(ETH_P_IP); 不知道为什么要设置一下。
3. 进入NF_INET_POST_ROUTING的netfilter进程处理。
4. ip_finish_output()
-> 对比skb->len和skb_dst(skb)->dev->mtu,如果需要分片,则调用ip_fragment()然后再ip_finish_output2()。
-> 如果不需要分片,直接交给ip_finish_output2()。

ip_finish_output2()
-> 没有足够的空间存放mac头?skb_realloc_headroom()。
-> 如果发现dst->hh缓存存在,说明MAC头的内容已经取得了,就用dst->hh->hh_data去填充MAC头,然后调用dst->hh->hh_output(skb),并返回。
-> 否则,调用dst->neighbour->output()方法,即neigh_resolve_output(),他会做四件事:
1. 将skb的data向后移skb->network_header - skb->data的距离,移动前data可能指向MAC头或者IP头,移动后指向IP头。
2. 将dst->hh->hh_output()赋值成neigh_resolve_output()或者dev_queue_xmit(),一般是后者。
3. dev->header_ops->create(skb, dev, type, daddr, saddr, len)创建以太网头,这个create方法是eth_header(),这个函数将skb->data前移14字节,然后给MAC头赋值。有6个参数:dev是发包设备,如eth1/br0/lo,以太网头的协议字段根据type的值(是否是802.3协议)判断填入type还是len,daddr为目的MAC,saddr为源MAC,如果saddr传入NULL则使用dev的地址。
4. 然后调用dst->neighbour->ops->queue_xmit(skb)发包,即dev_queue_xmit()。
总之,最终的结果是skb有了MAC头,并且skb->data指向了MAC头,并且交给了dev_queue_xmit(skb)。
上面这些函数指针都是引用的arp.c中定义的arp_generic_ops等arp_hh_ops等的成员。如arp_generic_ops的定义为:

static struct neigh_ops arp_generic_ops = {
    .family =       AF_INET,
    .solicit =      arp_solicit,
    .error_report =     arp_error_report,
    .output =       neigh_resolve_output,
    .connected_output = neigh_connected_output,
    .hh_output =        dev_queue_xmit,
    .queue_xmit =       dev_queue_xmit,
};

发包应该有主动和被动两种情况,一个是dev_queue_xmit()主动调用qdisc_run(),一个是软中断轮询函数net_tx_action()调用qdisc_run()。在qdisc_run()里面允许重新调度。
dev_queue_xmit():
如果设备有发包队列(设备的发包队列是struct netdev_queue结构),通过sch->enqueue()(指向pfifo_fast_enqueue())将数据包放入qdisc相连的input_pkt_queue队列(qdisc + sizeof(struct Qdisc)的位置)。然后调用qdisc_run(),其中qdisc_restart()会去从qdisc队列取skb(这里允许被reschedule),并用dev_hard_start_xmit(skb, dev, txq)发送出去。这里所说的发包队列是发包设备dev自己的发包队列,可以看到struct net_device结构有下面两个成员:
struct netdev_queue rx_queue;
struct netdev_queue *_tx ____cacheline_aligned_in_smp;
不知道rx_queue成员有没有用。
没有发包队列的,如lo就。。。。(还没看)
在netif_rx()里,也有将skb放到CPU的softnet_data->input_pkt_queue的队列里面去。而netif_receive_skb()是直接走收包流程。在我们的athr_receive_pkt()中使用的是后者,换成前者也可以。net_rx_action()对softnet_data->poll_list进行的操作 – 执行poll函数,这个poll函数就是process_backlog(),poll_list中的每个成员都是napi_struct结构体类型的。__napi_schedule(struct napi_struct *n)会将参数n加入到CPU的softnet_data的poll_list中,如我们的__athr_mac_rx_sched()就会调用__napi_schedule()。
没有太多地方会将数据包放到CPU的softnet_data中,估计就是netif_rx()和__napi_schedule(),同时net_rx_action()只处理CPU的softnet_data中的数据包。
那每个设备的struct netdev_queue队列应该就是单独的。也就是说:
如果使用athr_receive_pkt()收包,数据包会被放到发包设备的发包队列中,并由qdisc_restart()负责发送。
如果使用netif_rx()收包,数据包会被放到CPU的softnet_data的队列中,并由net_rx_action()负责发送。process_backlog()只会被net_rx_action()使用,而由于我们的以太网驱动收包函数直接调用的netif_receive_skb(skb)收包,所以net_rx_action()的机会并不多,加打印我只看到loopback_xmit()会调用netif_rx(),如ping自己的时候。这里没管无线驱动,无线驱动使用netif_rx将包交给协议栈的,不过我实验的时候把无线关了。
所以,如果将athr_receive_pkt()中的netif_receive_skb(skb)函数调用改为netif_rx(skb),你会发现以太网驱动上来的包都是由net_rx_action()处理的,因为netif_rx(skb)是将数据包放到CPU的softnet_data中。

上面说的两个队列(CPU的和dev的)只在接收和转发的时候用。那从外面收到的发往本机的包在经过L4层的时候,还会有一个队列,属于sock层,在struct sock结构里有几个队列,如sk->sk_receive_queue,sk->sk_backlog都是收包队列,在udp_rcv() -> … -> udp_queue_rcv_skb()函数里(又如tcp_v4_rcv() -> tcp_v4_do_rcv()中),下面的代码会把数据包放到队列里去:
UDP的:

    if (!sock_owned_by_user(sk))
        rc = __udp_queue_rcv_skb(sk, skb);
    else
        sk_add_backlog(sk, skb);

TCP的:

    if (!sock_owned_by_user(sk)) {
        {
            if (!tcp_prequeue(sk, skb))
                ret = tcp_v4_do_rcv(sk, skb);
        }
    } else
        sk_add_backlog(sk, skb);

而socket的用户态的接口函数如recv()和recvmsg()到内核中对应的tcp_recvmsg()和udp_recvmsg()会去从队列里拿包。
等待通过套接字交付数据的进程,都在sk->sk_sleep等待队列上睡眠,在__udp_queue_rcv_skb()将数据放到sk->sk_receive_queue之后,会调用sk->sk_data_ready回调函数(指向sock_def_readable())来通知套接字有新数据到达,这会唤醒在sk->sk_sleep睡眠且等待数据到达的进程。但是sk_add_backlog()只是简单的将数据放到sk->sk_backlog队列中。
通过socket发送数据时,发送的数据放在sk->sk_write_queue队列中。
注意,通过路由器转发的数据包,是不走L4层(即sock层)的,所以,有关socket的东西肯定都是本地收本地发的。

struct sock结构是socket在传输层的表示形式:
struct sock - network layer representation of sockets

socketcall系统调用:可以通过这一个系统调用实现socket用户态的所有操作,如send,recv等,但是没看到有谁用它。
————
一个网络设备是一个struct net_device结构。
alloc_netdev()分配一个struct net_device实例:alloc_netdev(sizeof_priv, name, setup)
并通过setup参数给新的设备做一些特定于设备类型的初始化,如对于以太网设备,setup是ether_setup()函数:

void ether_setup(struct net_device *dev)
{
    dev->header_ops     = &eth_header_ops;
    dev->type       = ARPHRD_ETHER;
    dev->hard_header_len    = ETH_HLEN;
    dev->mtu        = ETH_DATA_LEN;
    dev->addr_len       = ETH_ALEN;
    dev->tx_queue_len   = 1000; /* Ethernet wants good queues */
    dev->flags      = IFF_BROADCAST|IFF_MULTICAST;
    memset(dev->broadcast, 0xFF, ETH_ALEN);
}

以太网设备的type是ARPHRD_ETHER,MTU默认为1500,注意,MTU是网络设备的属性,所以想修改MTU要看该设备是否允许修改。
一个便捷的分配以太网设备的函数alloc_etherdev(sizeof_priv)用来分配一个名为eth%d的struct net_device实例,以及sizeof_priv字节的private数据,并调用ether_setup()。

register_netdev()或register_netdevice()将新设备注册进特定网络命名空间的全局设备列表中。他俩的区别通过register_netdev()的实现就可以看出来:

int register_netdev(struct net_device *dev)
{
    ...
    /*
     * If the name is a format string the caller wants us to do a
     * name allocation.
     */
    if (strchr(dev->name, '%')) {
        err = dev_alloc_name(dev, dev->name);
        if (err < 0)
            goto out;
    }
    err = register_netdevice(dev);
    ...
}

即register_netdev()可以处理用作接口名称的格式串,如eth%d。
注意,register_netdev()会去做rtnl_lock()和rtnl_unlock(),如果在ioctl里进行register_netdev()就会出错,因为ioctl之前设备也会rtnl_lock(),之后会rtnl_unlock(),所以这种情况下,请使用register_netdevice()。

注册设备完成后,网络命名空间设备列表中就有了该设备,看下面的代码可知共有三个列表记录了该设备的信息,即struct net结构的下面三个成员:
struct list_head dev_base_head;
struct hlist_head *dev_name_head;
struct hlist_head *dev_index_head;

static int list_netdevice(struct net_device *dev)
{
    struct net *net = dev_net(dev);

    ASSERT_RTNL();

    write_lock_bh(&dev_base_lock);
    list_add_tail(&dev->dev_list, &net->dev_base_head);
    hlist_add_head(&dev->name_hlist, dev_name_hash(net, dev->name));
    hlist_add_head(&dev->index_hlist, dev_index_hash(net, dev->ifindex)); //ifindex只是简单的+1来分配的。
    write_unlock_bh(&dev_base_lock);
    return 0;
}