Linux内核入门到放弃-网络-《深入Linux内核架构》笔记

网络命名空间

struct net {
    atomic_t        count;      /* To decided when the network
                         *  namespace should be freed.
                         */
    atomic_t        use_count;  /* To track references we
                         * destroy on demand
                         */
    struct list_head    list;       /* list of network namespaces *///所有命名空间链接到net_namespace_list上
    struct work_struct  work;       /* work struct for freeing */

    struct proc_dir_entry   *proc_net;
    struct proc_dir_entry   *proc_net_stat;
    struct proc_dir_entry   *proc_net_root;

    struct net_device       *loopback_dev;          /* The loopback */

    struct list_head    dev_base_head;      //与特定命名空间关联的所有设备
    struct hlist_head   *dev_name_head;     //用设备名作为散列键
    struct hlist_head   *dev_index_head;    //用接口索引作为散列键
};

网络子系统实现的所有全局函数,都需要一个网络命名空间作为参数,而网络子系统的所有全局属性,只能通过所述命名空间迂回访问。

套接字缓冲区

<linux/skbuff.h>
struct sk_buff {
    /* These two members must be first. */
    struct sk_buff      *next;
    struct sk_buff      *prev;

    struct sock     *sk;
    ktime_t         tstamp;
    struct net_device   *dev;

    struct  dst_entry   *dst;
    struct  sec_path    *sp;

    /*
     * This is the control buffer. It is free to use for every
     * layer. Please put your private variables there. If you
     * want to keep them across layers you have to do a skb_clone()
     * first. This is owned by whoever has the skb queued ATM.
     */
    char            cb[48];

    unsigned int        len,
                data_len;
    __u16           mac_len,
                hdr_len;
    union {
        __wsum      csum;
        struct {
            __u16   csum_start;
            __u16   csum_offset;
        };
    };
    __u32           priority;
    __u8            local_df:1,
                cloned:1,
                ip_summed:2,
                nohdr:1,
                nfctinfo:3;
    __u8            pkt_type:3,
                fclone:2,
                ipvs_property:1,
                nf_trace:1;
    __be16          protocol;

    void            (*destructor)(struct sk_buff *skb);
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
    struct nf_conntrack *nfct;
    struct sk_buff      *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
    struct nf_bridge_info   *nf_bridge;
#endif

    int         iif;
#ifdef CONFIG_NETDEVICES_MULTIQUEUE
    __u16           queue_mapping;
#endif
#ifdef CONFIG_NET_SCHED
    __u16           tc_index;   /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
    __u16           tc_verd;    /* traffic control verdict */
#endif
#endif
    /* 2 byte hole */

#ifdef CONFIG_NET_DMA
    dma_cookie_t        dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
    __u32           secmark;
#endif

    __u32           mark;

    sk_buff_data_t      transport_header;
    sk_buff_data_t      network_header;
    sk_buff_data_t      mac_header;
    /* These elements must be at the end, see alloc_skb() for details.  */
    sk_buff_data_t      tail;
    sk_buff_data_t      end;
    unsigned char       *head,
                *data;
    unsigned int        truesize;
    atomic_t        users;
};

套接字缓冲区用于在网络实现的各个层次之间交换数据,而无需来回复制分组数据,对性能的提高很可观。

使用套接字缓冲区管理数据

套接字缓冲区通过其中包含的各种指针与一个内存区域相关联,网络分组的数据就位于该区域中。

套接字缓冲区的基本思想是,通过操作指针来增删协议首部。

  • head和end指向数据在内存中的起始和结束位置
  • data和tail指向协议数据区域的起始和结束位置
  • mac_header指向MAC协议首部的起始,而network_header和transport_header分别指向网络层和传输层协议首部的起始。

对接收到的分组进行分析时,分组数据复制到内核分配的一个内存区中,并在整个分析期间一直处于该内存区中。与该分组相关联的套接字缓冲区在各层之间顺序传递,各层依次将其中的各个指针设置为正确值。

内核提供了一些用于操作套接字缓冲区的标准函数

  • alloc_skb:分配一个新的sk_buff实例
  • skb_copy:创建套接字缓冲区和相关数据的一个副本
  • skb_clone:创建套接字缓冲区的一个副本,但原本和副本将使用同一分组数据
  • skb_tailroom:返回数据末端空闲空间的长度
  • skb_headroom:返回数据起始处空闲空间的长度
  • skb_realloc_headroom:在数据起始处创建更多的空闲空间。现存数据不变

管理套接字缓冲区数据

套接字缓冲区结构不仅包含了上述指针,还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员。

  • tstamp:保存了分组到达的时间
  • dev指定了处理分组的网络设备
  • iff:输入设备的接口索引号
  • sk:指向用于处理该分组的套接字对应的socket实例
  • dst:表示接下来该分组通过内核网络实现的路由。
  • next和prev用于将套接字缓冲区保存到一个双链表中。这里没有使用内核的标准链表实现,而是使用了一个手工实现的版本。

使用了一个表头来实现套接字缓冲区的等待队列。其结构定义如下:

<linux/skbuff.h>
struct sk_buff_head {
    /* These two members must be first. */
    struct sk_buff  *next;
    struct sk_buff  *prev;

    __u32       qlen;
    spinlock_t  lock;
};

qlen指定了等待队列的长度,即队列中成员的数目。sk_buffer_head和sk_buff的next和prev用于创建一个循环双链表,套接字缓冲区的list成员回指到表头

网络访问层

网络设备的表示

在内核中,每个网络设备都表示为net_device结构的一个实例。在分配并填充该结构的一个实例后,必须用net/core/dev.c中的register_netdev函数将其注册到内核。

数据结构

<netdevice.h>
struct net_device
{
char name[IFNAMSIZ];
/* 设备名散列链表的链表元素 */
struct hlist_node name_hlist;

/* I/O相关字段*/
unsigned long mem_end;/*共享内存结束位置*/
unsigned long mem_start;/*共享内存起始位置*/
unsigned long base_addr;/*设备I/O地址*/
unsigned int irq;/*设备IRQ编号*/

unsigned long state;
struct list_head dev_list;
int (*init)(struct net_device *dev);

/* 接口索引。唯一的设备标识符*/
int ifindex;

struct net_device_stats* (*get_stats)(struct net_device *dev);

/* 硬件首部描述 */
const struct header_ops *header_ops;

unsigned short flags;//接口标志
unsigned mtu;//接口MTU
unsigned short type;//接口硬件类型
unsigned short hard_header_len;//硬件首部长度

/*接口地址信息*/
unsigned char perm_addr[MAX_ADDR_LEN];//持久硬件地址
unsigned char addr_len;//硬件地址长度
int promiscuity;

/* 协议相关指针 */
void *atalk_ptr;//AppleTalk相关指针
void *ip_ptr;//IPV4相关数据
void *dn_ptr;//DECnet相关数据
void *ip6_ptr;//IPv6相关数据
void *ec_ptr;//Econet相关数据

unsigned long last_rx;/* 上一次接收操作的时间 */
unsigned long trans_start;/* 上一次发送操作的时间(以 jiffies 为单位) */

/* eth_type_trans()所用的接口地址信息 */
unsigned char dev_addr[MAX_ADDR_LEN]; /* 硬件地址,(在bcast成员之前,因为大多数分组都是单播) */

unsigned char broadcast[MAX_ADDR_LEN]; /* 硬件多播地址 */

int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);

/* 在设备与网络断开后调用*/
void (*uninit)(struct net_device *dev);
/* 在最后一个用户引用消失后调用*/
void (*destructor)(struct net_device *dev);
/* 指向接口服务例程的指针 */

int (*open)(struct net_device *dev);
int (*stop)(struct net_device *dev);
void (*set_multicast_list)(struct net_device *dev);
int (*set_mac_address)(struct net_device *dev,void *addr);
int (*do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);
int (*set_config)(struct net_device *dev,struct ifmap *map);
int (*change_mtu)(struct net_device *dev, int new_mtu);
void (*tx_timeout) (struct net_device *dev);
int (*neigh_setup)(struct net_device *dev, struct neigh_parms *);

/* 该设备所在的网络命名空间 */
struct net *nd_net;

/* class/net/name项 */
struct device dev;

...
  • 在内核中,每个网卡都有唯一索引号,在注册时动态分配保存在ifindex成员中。
  • mtu:指定一个传输帧的最大长度
  • type:保存设备的硬件类型,如:ARPHD_ETHER和ARPHDR_IEEE802
  • dev_addr:存储设备的硬件地址,而addr_len指定该地址的长度

net_device结构的大多数成员都是函数指针,执行与网卡相关的典型任务。尽管不同适配器的实现各有不同,但调用的语法总是相同。因而这些成员表示了与下一个协议层次的抽象接口。这些接口使得内核能够用同一组接口函数来访问所有的网卡,而网卡的驱动程序负责实现细节。

  • open和stop分别初始化和终止网卡。
  • hard_start_xmit用于从等待队列删除已完成的分组并将其发送出去。
  • header_ops 是一个指向结构的指针,该结构提供了更多的函数指针,用于操作硬件首部。其中最重要的是 header_ops->create 和 header_ops->parse ,前者创建一个新的硬件首部,后者分析一个给定的硬件首部。
  • get_stats 查询统计数据,并将数据封装到一个类型为 net_device_stats 的结构中返回
  • 调用 tx_timeout 来解决分组传输失败的问题
  • do_ioctl 将特定于设备的命令发送到网卡。
  • nd_net 是一个指针,指向设备所属的网络命名空间。

注册网络设备

每个网络设备都按照如下过程注册

  • alloc_netdev分配一个新的struct net_device实例,一个特定于协议的函数用典型值填充该结构。对于以太网设备,该函数是ether_setup.
  • 在struct net_device填充完毕后,需要用register_netdev或reigster_netdevice注册。

register_netdevice的各个处理如下:

如果 net_device->init 提供了特定于设备的初始化函数,那么内核在进一步处理之前,将先调用该函数。由 dev_new_index 生成在所属命名空间中唯一标识该设备的接口索引。该索引保存在net_device->ifindex 中。在确保所选择的名称尚未使用,而且没有指定自相矛盾的设备特性(所支持特性的列表,请参见 <netdevice.h> 中的 NETIF_F_* )后,用 netdev_register_kobject 将新设备添加到通用内核对象模型中。该函数还会创建上文提到的sysfs项。最后,该设备集成到特定命名空间的链表中,以及以设备名和接口索引为散列键的两个散列表中。

接受分组

传统方法

(参考/drivers/net/isa-skeleton.c)

即每一个到达的分组都会产生中断

在中断上下文中,数据由3个短函数处理。执行了下列任务:

  • net_interrupt是由设备驱动程序设置的中断处理程序。它将确保该中断是否真的是由接收到的分组引发的。如果是,则调用net_rx.
  • net_rx函数也是特定于网卡的,首先创建一个套接字缓冲区。分组的内容接下来从网卡传输到缓冲区,然后使用内核源代码中针对各种传输类型的库函数来分析首部数据。这项分析将确定分组数据所使用的网络层协议,如IP协议。
  • 与上述两个方法不同, netif_rx 函数不是特定于网络驱动程序的,该函数位于 net/core/dev.c 。调用该函数,标志着控制由特定于网卡的代码转移到了网络层的通用接口部分。该函数的作用在于,将接收到的分组放置到一个特定于CPU的等待队列上(struct softnet),并退出中断上下文,使得CPU可以执行其他任务。
<netdevice.h>
struct softnet_data
{
...
    struct sk_buff_head input_pkt_queue;
...
};

netif_rx 在结束工作之前将软中断 NET_RX_SOFTIRQ 标记为即将执行,net_rx_action 用作该软中断的处理程序。

net_rx_action在一些准备任务之后,工作转移到 process_backlog ,该函数在循环中执行下列步骤

  • __skb_dequeue 从等待队列移除一个套接字缓冲区,该缓冲区管理着一个接收到的分组
  • 由 netif_receive_skb 函数分析分组类型,以便根据分组类型将分组传递给网络层的接收函数(即传递到网络系统的更高一层)。

所有用于从底层的网络访问层接收数据的网络层函数都注册在一个散列表中,通过全局数组ptype_base 实现

新的协议通过 dev_add_pack 增加。各个数组项的类型为 struct packet_type ,定义如下:

<netdevice.h>
struct packet_type {
    __be16 type;/* 这实际上是htons(ether_type)的值。 */
    struct net_device *dev;/* NULL在这里表示通配符 */
    int         (*func) (struct sk_buff *,
                         struct net_device *,
                         struct packet_type *,
                         struct net_device *);
...
    void *af_packet_priv;
    struct list_head list;
};

type指定了协议的标识符。dev 将一个协议处理程序绑定到特定的网卡( NULL 指针表示该处理程序对系统中所有网络设备都有效)。

func 是该结构的主要成员。它是一个指向网络层函数的指针,如果分组的类型适当,将其传递给该函数。

netif_receive_skb 对给定的套接字缓冲区查找适当的处理程序,并调用其 func 函数,将处理分组的职责委托给网络层,这是网络实现中更高的一层。

对高速接口的支持(NAPI)

假定某个网络适配器此前没有分组到达,但从现在开始,分组将以高频率频繁到达。这就是NAPI设备的情况,如下所述。

  • 第一个分组将导致网络适配器发出IRQ。为防止进一步的分组导致发出更多的IRQ,驱动程序会关闭该适配器的Rx IRQ。并将该适配器放置到一个轮询表上。
  • 只要适配器上还有分组需要处理,内核就一直对轮询表上的设备进行轮询。
  • 重新启用Rx中断。

只有设备满足如下两个条件时,才能实现NAPI方法。

  • 设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中。下文将该缓冲区称为Rx缓冲区。
  • 该设备必须能够禁用用于分组接收的IRQ。而且,发送分组或其他可能通过IRQ进行的操作,都仍然必须是启用的。

如果一个分组到达一个空的Rx缓冲区,则将相应的设备置于轮询表中。

内核以循环方式处理链表上的所有设备:内核依次轮询各个设备,如果已经花费了一定的时间来处理某个设备,则选择下一个设备进行处理。此外,某个设备都带有一个相对权重,表示与轮询表中其他设备相比,该设备的相对重要性。较快的设备权重较大,较慢的设备权重较小。由于权重指定了在一个轮询的循环中处理多少分组,这确保了内核将更多地注意速度较快的设备。

与旧的API相比,关键性的变化在于,支持NAPI的设备必须提供一个 poll 函数。该方法是特定于设备的,在用 netif_napi_add注册网卡时指定。调用该函数注册,表明设备可以且必须用新方法处理。

<netdevice.h>
static inline void netif_napi_add(struct net_device *dev,
                                  struct napi_struct *napi,
                                  int (*poll)(struct napi_struct *, int),
                                  int weight);

dev 指向所述设备的 net_device 实例, poll 指定了在IRQ禁用时用来轮询设备的函数, weight指定了设备接口的相对权重。无论如何,权重都不能超过该设备可以在Rx缓冲区中存储的分组的数目。

netif_napi_add 还需要另一个参数,是一个指向 struct napi_struct 实例的指针。该结构用于管理轮询表上的设备。

<linux/netdevice.h>
struct napi_struct {
    /* The poll_list must only be managed by the entity which
     * changes the state of the NAPI_STATE_SCHED bit.  This means
     * whoever atomically sets that bit can add this napi_struct
     * to the per-cpu poll_list, and whoever clears that bit
     * can remove from the list right before clearing the bit.
     */
    struct list_head    poll_list;

    unsigned long       state;
    int         weight;
    int         (*poll)(struct napi_struct *, int);
#ifdef CONFIG_NETPOLL
    spinlock_t      poll_lock;
    int         poll_owner;
    struct net_device   *dev;
    struct list_head    dev_list;
#endif
};

轮询表通过一个标准的内核双链表实现, poll_list 用作链表元素.state 可以是 NAPI_STATE_SCHED 或 NAPI_STATE_DISABLE ,前者表示设备将在内核的下一次循环时被轮询,后者表示轮询已经结束且没有更多的分组等待处理,但设备尚未从轮询表移除。

请注意, struct napi_struct 经常嵌入到一个更大的结构中,后者包含了与网卡有关的、特定于驱动程序的数据。这样在内核使用 poll 函数轮询网卡时,可用 container_of 机制获得相关信息。

实现poll函数

poll 函数需要两个参数:一个指向 napi_struct 实例的指针和一个指定了“预算”的整数,预算表示内核允许驱动程序处理的分组数目。我们并不打算处理真实网卡的可能的奇异之处,因此讨论一个伪函数,该函数用于一个需要NAPI的超高速适配器:

static int hyper_card_poll(struct napi_struct *napi, int budget)
{
    struct nic *nic = container_of(napi, struct nic, napi);
    struct net_device *netdev = nic->netdev;
    int work_done;
    
    work_done = hyper_do_poll(nic, budget);
    
    if (work_done < budget) {
        netif_rx_complete(netdev, napi);
        hcard_reenable_irq(nic);
    }
    return work_done;
}

在从 napi_struct 的容器获得特定于设备的信息之后,调用一个特定于硬件的方法(这里是hyper_do_poll )来执行所需要的底层操作从网络适配器获取分组,并使用像此前那样使用netif_receive_skb 将分组传递到网络实现中更高的层。

实现IRQ处理程序

NAPI也需要对网络设备的IRQ处理程序做一些改动。这里仍然不求助于任何具体的硬件,而介绍针对虚构设备的代码:

static irqreturn_t e100_intr(int irq, void *dev_id)
{
    struct net_device *netdev = dev_id;
    struct nic *nic = netdev_priv(netdev);
    
    if(likely(netif_rx_schedule_prep(netdev, &nic->napi))) {
        hcard_disable_irq(nic);
        __netif_rx_schedule(netdev, &nic->napi);
    }
    return IRQ_HANDLED;
}

假定特定于接口的数据保存在 net_device->private 中,这是大多数网卡驱动程序使用的方法。使用辅助函数 netdev_priv 访问该字段。

现在需要通知内核有新的分组可用。这需要如下二阶段的方法。

  • netif_rx_schedule_prep 准备将设备放置到轮询表上。本质上,这会安置 napi_struct->flags 中的 NAPI_STATE_SCHED 标志。
  • 如果设置该标志成功(仅当NAPI已经处于活跃状态时,才会失败),驱动程序必须用特定于设备的适当方法来禁用相应的IRQ。调用 __netif_rx_schedule 将设备的 napi_struct 添加到轮询表,并引发软中断 NET_RX_SOFTIRQ 。这通知内核在 net_rx_action 中开始轮询。

处理Rx软中断

在讨论了为支持NAPI驱动程序需要做哪些改动之后,我们来考察一下内核需要承担的职责。net_rx_action 依旧是软中断 NET_RX_SOFTIRQ 的处理程序。在前一节给出了该函数的一个简化版本。随着有关NAPI的更多细节尘埃落定,现在可以讨论该函数的所有细节了。

本质上,内核通过依次调用各个设备特定的 poll 方法,处理轮询表上当前的所有设备。设备的权重用作该设备本身的预算,即轮询的一步中可能处理的分组数目。

必须确保在这个软中断的处理程序中,不会花费过多时间。如果如下两个条件成立,则放弃处理。

  • 处理程序已经花费了超出一个 jiffie 的时间。
  • 所处理分组的总数,已经超过了 netdev_budget 指定的预算总值。通常,总值设置为300,但可以通过 /proc/sys/net/core/netdev_budget 修改。

在轮询了一个设备之后,内核会检查所处理的分组数目,与该设备的预算是否相等。如果相等,那么尚未获得该设备上所有等待的分组,即代码流程图中 work == weight 所表示的情况。内核接下来将该设备移动到轮询表末尾,在链表中所有其他设备都处理过之后,继续轮询该设备。显然,这实现了网络设备之间的循环调度。

发送分组

net/core/dev.c 中的 dev_queue_xmit 用于将分组放置到发出分组的队列上。这里将忽略这个特定于设备的队列的实现,因为它并没有揭示什么网络层的运作机制。只要知道,在分组放置到等待队列上一定的时间之后,分组将发出即可。这是通过特定于适配器的函数 hard_start_xmit 完成的,在每个 net_device 结构中都以函数指针的形式出现,由硬件设备驱动程序实现。

网络层

在内核源代码中,首部由iphdr数据结构实现

<linux/ip.h>
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:4,
        version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
    __u8    version:4,
        ihl:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
    __u8    tos;
    __be16  tot_len;
    __be16  id;
    __be16  frag_off;
    __u8    ttl;
    __u8    protocol;
    __sum16 check;
    __be32  saddr;
    __be32  daddr;
    /*The options start here. */
};

ip_rcv 函数是网络层的入口点。

接收分组

ip_local_deliver、ip_forward

交付到本地传输层

分片合并

ip_defrag 重新组合分片分组的各个部分。

<net/ipv4/ip_fragment.c>
int ip_defrag(struct sk_buff *skb, u32 user)
{
    struct ipq *qp;

    IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);

    /* Start by cleaning up the memory. */
    if (atomic_read(&ip4_frags.mem) > ip4_frags_ctl.high_thresh)
        ip_evictor();

    /* Lookup (or create) queue header */
    if ((qp = ip_find(ip_hdr(skb), user)) != NULL) {
        int ret;

        spin_lock(&qp->q.lock);

        ret = ip_frag_queue(qp, skb);

        spin_unlock(&qp->q.lock);
        ipq_put(qp);
        return ret;
    }

    IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
    kfree_skb(skb);
    return -ENOMEM;
}

内核在一个独立的缓存中管理原本属于一个分组的各个分片,该缓存称为分片缓存(fragment cache)。在缓存中,属于同一分组的各个分片保存在一个独立的等待队列中(struct ipq),直至该分组的所有分片都到达。

ip_find 函数。它使用一个基于分片ID、源地址、目标地址、分组的协议标识的散列过程,检查是否已经为对应的分组创建了等待队列。如果没有,则建立一个新的队列,并将当前处理的分组置于其上。 否则返回现存队列的地址,以便 ip_frag_queue 将分组置于队列上。

在分组的所有分片都进入缓存(即第一个和最后一个分片都已经到达,且所有分片中数据的长度之和等于分组预期的总长度)后, ip_frag_reasm 将各个分片重新组合起来。接下来释放套接字缓冲区,供其他用途使用。

如果分组的分片尚未全部到达,则 ip_defrag 返回一个 NULL 指针,终止互联网络层的分组处理。在所有分片都到达后,将恢复处理。

交付到传输层

ip_local_deliver->ip_local_deliver_finish

其中,根据分组的协议标识符确定一个传输层的函数,将分组传递给该函数。所有基于互联网的协议都有一个net_protocol结构的实例,该结构定义如下:

<include/net/protocol.h>
struct net_protocol {
    int         (*handler)(struct sk_buff *skb);
    void            (*err_handler)(struct sk_buff *skb, u32 info);
    int         (*gso_send_check)(struct sk_buff *skb);
    struct sk_buff         *(*gso_segment)(struct sk_buff *skb,
                           int features);
    int         no_policy;
};
  • handler是协议处理函数,分组将被传递到该函数进一步处理
  • 在接收到ICMP错误信息并需要传递到更高层时,需要调用 err_handler 。

inet_add_protocol 标准函数用于将上述结构的实例(指针)存储到 inet_protos 数组中,通过一种散列方法确定存储具体协议的索引位置。

分组转发

ip_forward

首先,该函数根据 TTL 字段来检查当前分组是否允许传输到下一跳。如果 TTL 值小于或等于1,则丢弃分组,否则,将 TTL 计数器值减1。ip_decrease_ttl 负责该工作,修改 TTL 字段的同时,分组的校验和也会发生变化,同样需要修改。

在调用netfilter挂钩 NF_IP_FORWARD 后,内核在 ip_forward_finish 中恢复处理。该函数将其工作委托给如下两个函数。

  • 如果分组包含额外的选项(通常情况下没有),则在 ip_forward_options 中处理。
  • dst_pass 将分组传递到在路由期间选择、保存在 skb->dst->output 中的发送函数。通常使用ip_output ,该函数将分组传递到与目标地址匹配的网络适配器。 下一节描述的IP分组发送操作中, ip_output 是其中一部分。

发送分组

内核提供了几个通过互联网络层发送数据的函数,可由较高协议层使用。其中 ip_queue_xmit 是最常使用的一个。

ip_queue_xmit的第一个任务是查找可用于该分组的路由。内核利用了下述事实:起源于同一套接字的所有分组的目标地址都是相同的,这样不必每次都重新确定路由。下文将讨论指向相应数据结构的一个指针,它与套接字数据结构相关联。在发送第一个分组时,内核需要查找一个新的路由(在下文讨论)。在 ip_send_check 为分组生成校验和之后, 内核调用netfilter挂钩 NF_IP_LOCAL_OUT 。接下来调用 dst_output 函数。该函数基于确定路由期间找到的 skb->dst->output函数,后者位于套接字缓冲区中,与目标地址相关。通常,该函数指针指向 ip_output ,本地产生和转发的分组将在该函数中汇集。

转移到网络访问层

ip_output函数:

首先调用netfilter挂钩 NF_IP_POST_ROUTING ,接下来是 ip_finish_output。首先考察分组长度不大于传输介质MTU、无须分片的情况。在这种情况下,直接调用了 ip_finish_output2 。

该函数检查套接字缓冲区是否仍然有足够的空间容纳产生的硬件首部。如有必要,则用 skb_realloc_headroom分配额外的空间。为完成到网络访问层的转移,调用由路由层设置的函数 dst->neighbour->output ,该函数指针通常指向 dev_queue_xmit 。 1

分组分片

ip_fragment将IP分组划分为更小的单位,

如果忽略RFC 791中记载的各种微妙情形,那么IP分片是非常简单的。在循环的每一轮中,都抽取出一个数据分片,其长度与对应的MTU兼容。创建一个新套接字缓冲区来保存抽取的数据分片,旧的IP首部可以稍作修改后重用。所有的分片都会分配一个共同的分片ID,以便在目标系统上重新组装分组。分片的顺序基于分片偏移量建立,此时也需要适当地设置。 MF (more fragments)标志位也需要设置。只有序列中的最后一个分片可以将该标志位置0。每个分片都在使用 ip_send_check 产生校验和之后,用 ip_output 发送

路由

如果分组的目标系统与本地主机直接连接,路由通常特化为查找对应的网卡。否则,必须根据路由选择信息来查找网关系统(以及与网关相关联的网卡),分组需要通过网关来发送。

路由的起始点是 ip_route_input 函数,它首先试图在路由缓存中查找路由。

ip_route_input_slow 用于根据内核的数据结构来建立一个新的路由。基本上,该例程依赖于fib_lookup ,后者的隐式返回值(通过一个用作参数的指针)是一个 fib_result 结构的实例,包含了我们需要的信息。 fib(forward information database) 代表转发信息库,是一个表,用于管理内核保存的路由选择信息。

路由结果关联到一个套接字缓冲区,套接字缓冲区的 dst 成员指向一个 dest_entry 结构的实例,该实例的内容是在路由查找期间填充的。该数据结构的定义如下(简化了很多):

include/net/dst.h
struct dst_entry
{
    struct net_device *dev;
    int (*input)(struct sk_buff*);
    int (*output)(struct sk_buff*);
    struct neighbour *neighbour;
};
  • input 和 output 分别用于处理进入和外出的分组,如上文所述。
  • dev 指定了用于处理该分组的网络设备。

根据分组的类型,会对 input 和 output 指定不同的函数。

  • 对需要交付到本地的分组, input 设置为 ip_local_deliver ,而 output 设置为 ip_rt_bug (该函数只向内核日志输出一个错误信息,因为在内核代码中对本地分组调用 output 是一种错误,不应该发生)。
  • 对于需要转发的分组, input 设置为 ip_forward ,而 output 设置为 ip_output 函数。

neighbour 成员存储了计算机在本地网络中的IP和硬件地址,这可以通过网络访问层直接到达。对我们来说,只考察该结构的几个成员就足够了:

include/net/neighbour.h
struct neighbour
{
    struct net_device *dev;
    unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
    int (*output)(struct sk_buff *skb);
};

dev 保存了网络设备的数据结构,而 ha 是设备的硬件地址, output 是指向适当的内核函数的指针,在通过网络适配器传输分组时必须调用。 neighbour 实例由内核中实现ARP(address resolution protocol,地址转换协议)的ARP层创建,ARP协议负责将IP地址转换为硬件地址。因为 dst_entry 结构有一个成员指针指向 neighbour 实例,网络访问层的代码在分组通过网络适配器离开当前系统时可调用 output 函数。

用hash表实现的路由表,其数据结构关系可参考:< > p900.

netfilter

扩展网络功能

简言之,netfilter框架向内核添加了下列能力:

  • 根据状态及其他条件,对不同数据流方向(进入、外出、转发)进行分组过滤 (packet filtering)。
  • NAT(network address translation,网络地址转换),根据某些规则来转换源地址和目标地址。
  • 分组处理(packet manghing)和操作(manipulation),根据特定的规则拆分和修改分组。

netfilter实现划分为如下两个部分。

  • 内核代码中的挂钩,位于网络实现的核心,用于调用netfilter代码。
  • netfilter模块,其代码挂钩内部调用,但其独立于其余的网络代码。一组标准模块提供了常用的函数,但可以在扩展模块中定义用户相关的函数。

调用挂钩函数

netfilter挂钩通过 <netfilter.h> 中的 NF_HOOK 宏调用。如果内核启用的netfilter支持,该宏定义如下:

<linux/netfilter.h>
static inline int nf_hook_thresh(int pf, unsigned int hook,
                 struct sk_buff *skb,
                 struct net_device *indev,
                 struct net_device *outdev,
                 int (*okfn)(struct sk_buff *), int thresh,
                 int cond)
{
    if (!cond)
        return 1;
#ifndef CONFIG_NETFILTER_DEBUG
    if (list_empty(&nf_hooks[pf][hook]))
        return 1;
#endif
    return nf_hook_slow(pf, hook, skb, indev, outdev, okfn, thresh);
}

#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh)         \
({int __ret;                                       \
if ((__ret=nf_hook_thresh(pf, hook, (skb), indev, outdev, okfn, thresh, 1)) == 1)\
    __ret = (okfn)(skb);                               \
__ret;})

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
    NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)

考虑 NF_HOOK_THRESH 的实现。首先调用了 nf_hook_thresh 。该函数首先检查 cond 中给定的条件是否为真。如果不是,则直接向调用者返回1。否则,调用 nf_hook_slow 。该函数遍历所有注册的netfilter挂钩并调用它们。如果分组被接受,则返回1,否则返回其他的值。

如果 nf_hook_thresh 返回1,即netfilter判定接受该分组,那么控制传递到 okfn 中指定的结束处理函数。

扫描挂钩表

如果至少注册了一个挂钩函数并需要调用,那么会调用 nf_hook_slow 。所有挂钩都保存在二维数组 nf_hooks 中:

<net/netfilter/core.c>
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS] __read_mostly;

NPROTO 指定系统支持的协议族的最大数目,每个协议可以定义 NF_MAX_HOOKS 个挂钩链表,默认值是8个。

该表的 list_head 元素作为双链表表头,双链表中可容纳 nf_hook_ops 实例:

<linux/netfilter.h>

struct nf_hook_ops
{
    struct list_head list;

    /* User fills in from here down. */
    nf_hookfn *hook;
    struct module *owner;
    int pf;
    int hooknum;
    /* Hooks are ordered in ascending priority. */
    int priority;
};
  • hook 是一个指向挂钩函数的指针,它需要的参数与 NF_HOOK 宏相同:
  • pf 和 hooknum 指定了协议族和与挂钩相关的编号。这信息还可以从挂钩链表在 nf_hooks 中的位置推断出来。
  • 链表中的挂钩是按照优先级升序排列的(优先级由 priority 表示)。整个 signed int 类型的范围都可用于表示优先级,内核也定义了一些推荐使用的默认值:

激活挂钩函数

每个 hook 函数都返回下列值之一。

  • NF_ACCEPT 表示接受分组。这意味着所述例程没有修改数据。内核将继续使用未修改的分组,使之穿过网络实现中剩余的协议层(或通过后续的挂钩)。
  • NF_STOLEN 表示挂钩函数“窃取”了一个分组并处理该分组。此时,该分组已与内核无关,不必再调用其他挂钩。还必须取消其他协议层的处理。
  • NF_DROP 通知内核丢弃该分组。如同 NF_STOLEN ,其他挂钩或网络层的处理都不再需要了。套接字缓冲区(和分组)占用的内存空间可以释放,因为其中包含的数据可以被丢弃,例如,挂钩可能认定分组是损坏的。
  • NF_QUEUE 将分组置于一个等待队列上,以便其数据可以由用户空间代码处理。不会执行其他挂钩函数。
  • NF_REPEAT 表示再次调用该挂钩。

最终,除非所有挂钩函数都返回 NF_ACCEPT ( NF_REPEAT 不是最终结果),否则分组不会在网络子系统进一步处理。所有其他的分组,不是被丢弃,就是由netfilter子系统处理

内核提供了一个挂钩函数的集合,使得不必为每个场合都单独定义挂钩函数。这些称为iptables,用于分组的高层处理。它们使用用户空间工具iptables配置,这里不讨论该工具。

传输层

UDP

udp_rcv -> __udp4_lib_rcv:udp_rcv 只是 __udp4_lib_rcv 的一个包装器,该函数的输入参数是一个套接字缓冲区。在确认分组未经篡改之后,必须用 __udp4_lib_lookup 查找与之匹配的监听套接字。

net/ipv4/udp.c 中的 __udp4_lib_lookup 用于查找与分组目标匹配的内核内部的套接字。在有某个监听进程对该分组感兴趣时,在 udphash 全局数组中则会有与分组目标端口匹配的 sock 结构实例, __udp4_lib_lookup 可采用散列方法查找并返回该实例。如果找不到这样的套接字,则向源系统发送一个“目标不可到达”的消息,并丢弃分组的内容。

内核中有两种数据结构用于表示套接字。 sock 是到网络访问层的接口,而 socket 是到用户空间的接口。

当前, sock 结构可以简化为下列缩略版:

include/net/sock.h
/* 简化版 */
struct sock {
    wait_queue_head_t *sk_sleep;
    struct sk_buff_head sk_receive_queue;

    /* 回调 */
    void (*sk_data_ready)(struct sock *sk, int bytes);
}

在 udp_rcv 查找到适当的 sock 实例后,控制转移到 udp_queue_rcv_skb ,而后又立即到 sock_queue_rcv_skb ,其中会执行两个重要的操作,完成到应用层的数据交付。

  • 等待通过套接字交付数据的进程,在 sk_sleep 等待队列上睡眠。
  • 调用 skb_queue_tail 将包含分组数据的套接字缓冲区插入到 sk_receive_queue 链表末端,其表头保存在特定于套接字的 sock 结构中.
  • 调用 sk_data_ready 指向的函数(如果用标准函数 sock_init_data 来初始化 sock 实例,通常是 sock_def_readable ),通知套接字有新数据到达。这会唤醒在 sk_sleep 队列上睡眠、等待数据到达的所有进程。

TCP

接收TCP数据

系统中的每个TCP套接字都归入3个散列表之一,分别接受下列状态的套接字。

  • 完全连接的套接字。
  • 等待连接(监听状态)的套接字。
  • 处于建立连接过程中(使用下文讨论的三次握手)的套接字。

tcp_v4_rcv:

在对分组数据进行各种检查并将首部中的信息复制到套接字缓冲区的控制块之后,内核将查找等待该分组的套接字的工作委托给 __inet_lookup 函数。该函数唯一的任务就是调用另两个函数,扫描各种散列表。 __inet_lookup_established 企图返回一个已连接的套接字。如果没有找到适当的结构,则调用 inet_lookup_listener 函数检查所有的监听套接字。

在两种情况下,这些函数合并考虑了对应连接的各种不同因素(客户机和服务器的IP地址、网络接口的端口地址和内核内部的索引),通过散列函数来查找一个前述的 sock 类型实例。在搜索监听的套接字时,会针对与通配符匹配的几个套接字,应用计分方法来查找其中最佳的候选者。由于其结果只反映了从直觉上认定的最佳候选者,这里不讨论该主题。

与UDP相比,在找到对应该连接的适当的 sock 结构之后,工作尚未结束,只是新的工作的开始。取决于连接的状态,必须进行如图12-25所示的状态迁移。 tcp_v4_do_rcv 是一个多路分解器,基于套接字状态将代码控制流划分为不同的分支。

三次握手

  • 客户端通过向服务器 发送SYN来发出连接请求。客户端的套接字状态由CLOSED变为SYN_SENT。
  • 服务器在一个监听套接字上接收到连接请求,并返回SYN和ACK。 服务器套接字的状态由LISTEN变为SYN_REC。
  • 客户端套接字接收到SYN/ACK分组后,切换到ESTABLISHED状态,表明连接已经建立。一个ACK分组被发送到服务器。
  • 服务器接收到ACK分组,也切换到ESTABLISHED状态。这就完成了两端的连接建立工作,可以开始数据交换。

被动连接建立

tcp_v4_rcv --> tcp_v4_do_rcv

tcp_v4_do_rcv --> tcp_v4_hnd_req

tcp_v4_do_rcv --> tcp_rcv_state_process

调用 tcp_v4_hnd_req 来执行网络层中建立新连接所需的各种初始化任务。

实际的状态迁移发生在 tcp_rcv_state_process 中,该函数由一个长的 switch/case 语句组成,区分各种可能的套接字状态来调用适当的传输函数。

如果套接字状态是 TCP_LISTEN ,则调用 tcp_v4_conn_request 。 1 该函数处理了TCP的许多细节和微妙之处,在这里不描述了。重要的是该函数结束前发送的确认分组。其中不仅包含了设置的ACK标志和接收到的分组的序列号,还包括新生成的序列号和SYN标志,这是三次握手过程的要求。这样就完成了连接建立的第一阶段。

客户端的下一步是,接收通过通常的路径到达 tcp_rcv_state_process 的ACK分组。套接字状态现在是 TCP_SYN_RECV ,由一个特定的 case 分支处理。内核的主要任务是将套接字状态修改为TCP_ESTABLISHED ,表示连接现在已经建立。

主动建立连接

tcp_v4_connect:

该函数开始于查找到目标主机的IP路由,使用的框架如上所述。在产生TCP首部并将相关的值设置到套接字缓冲区中之后,套接字状态从 CLOSED 改变为 SYN_SENT 。接下来 tcp_connect 将一个SYN分组发送到互联网络层,接下来到服务器端。此外,在内核中创建一个定时器,确保如果在一定的时间内没有接收到确认,将重新发送分组。

现在客户端必须等待服务器对SYN分组的确认以及确认连接请求的一个SYN分组,这是通过普通的TCP机制接收的(图12-29的下半部)。这又通向了 tcp_rcv_state_process 分配器,在这种情况下,控制流又转移到 tcp_rcv_synsent_state_process 。套接字状态设置为 ESTABLISHED ,而 tcp_send_ack 向服务器返回另一个ACK分组,完成连接建立。

分组传输

TCP有几个特性:

  • 按可保证的次序传输字节流。
  • 通过自动化机制重传丢失的分组。
  • 每个方向上的数据流都独立控制,并与对应主机的速度匹配。

尽管最初这些需求可能看起来并不复杂,但满足这些要求需要的过程和技巧是相对较多的。因为大多数连接是基于TCP的,实现的速度和效率是关键性的,所以Linux内核借助于技巧和优化。遗憾的是,这未必能使实现更容易理解。

基于序列号来确认分组的概念,也用于普通的分组。但与上文提到的内容相比,序列号揭示了有关数据传输的更多东西。序列号根据何种方案分配?在建立连接时,生成一个随机数(由内核使用drivers/char/random.c 中的 secure_tcp_sequence_number 生成)。接下来使用一种系统化的方法来支持对所有进入分组的严格确认。

ACK数目也表示了下一个字节的索引号。

接收分组

tcp_rcv_established 函数,再次将控制流分裂开来。易于分析的分组在快速路径(fast path)中处理,而包含了不常见选项的分组在低速路径(slow path)处理。

分组需要符合下列条件之一,才能归类为易于分析的。:

  • 分组必须只包含对上一次发送数据的确认。
  • 分组必须只包含预期将接收的数据。

此外,下列标志都不能设置:SYN、URG、RST或FIN。

快速路径的代码并不处理ACK部分,该任务委托给 tcp_ack 。在这里,过时的分组以及由于接收方的TCP实现缺陷或传输错误和超时等造成的发送过早的分组,都被过滤出去。该函数最重要的任务不仅包括分析有关连接的新信息(例如,接收窗口信息)和其他TCP协议的微妙之处,还需要从重传队列中删除确认数据(在下文讨论)。该队列包含了所有发送的分组,如果在一定的时间限制内没有收到ACK确认,则需要重传。

在低速路径中,数据不能直接转发到套接字,因为必须对分组选项进行复杂的检查,而后可以是TCP子系统的响应。不按序到达的数据放置到一个专门的等待队列上,直至形成一个连续的数据段,才会被处理。只有到那时,才能将完整的数据传递到套接字。

发送分组

tcp_sendmsg

很自然,在数据传输可以开始之前,所用套接字的状态必须是 TCP_ESTABLISHED 。如果不是这样,内核将等待(借助于 wait_for_tcp_connect ),直到连接已经建立。数据接下来从用户空间进程的地址空间复制到内核空间,用于建立一个TCP分组。这里不打算讨论这个复杂的操作,因为其中涉及大量过程,所有这些的目的都是满足TCP协议的复杂需求。遗憾的是,发送TCP分组的工作,并不仅仅限于构建一个首部并转入互联网络层。还必须遵守下列需求(这绝不是完备的列表)。

  • 接收方等待队列上必须有足够的空间可用于该数据。
  • 必须实现防止连接拥塞的ECN机制
  • 必须检测某一方出现失效的情况,以免通信出现停顿
  • TCP慢启动(slow-start)机制要求在通信开始时,逐渐增大分组长度。
  • 发送但未得到确认的分组,必须在一定的超时时间间隔之后反复重传,直至接收方最终确认。

由于重传队列是通过TCP连接进行可靠数据传输的关键要素,所以这里详细讲述一下它的工作机制。在分组装配完毕之后,内核到达 tcp_push_one ,该函数执行下列3个任务。

  • tcp_snd_test 检查目前是否可以发送数据。接收方过载导致的分组积压,可能使得现在无法发送数据。
  • tcp_transmit_skb 使 用 地 址 族 相 关 的 af_specific->queue_xmit 函 数 ( IPv4 使 用 的 是ip_queue_xmit ),将数据转发到互联网络层。
  • update_send_head 处理对统计量的更新。更重要的是,它会初始化所发送TCP信息段(TCPsegment)的重传定时器。不必对每个TCP分组都这样做,该机制只用于已经确认的数据区之后的第一个分组。

inet_csk_reset_xmit_timer 负责重置重传定时器。该定时器是未确认分组重发的基础,是TCP传输的一种保证。如果接收方在一定的时间内没有确认收到数据,则重传数据。

与特定套接字关联的 sock 实例中包含了一个重传计时器的链表,用于发送的每个分组。

内核使用的超时函数是 tcp_write_timer ,如果没有收到ACK,该函数会调用 tcp_retransmit_timer 函数。在重传数据时,必须注意下列问题。

  • 连接在此期间可能已经关闭。在这种情况下,保存的分组和定时器将从内核内存中删除。
  • 如果重传尝试的次数超过了 sysctl_tcp_retries2 变量指定的限制,则放弃重传。

连接终止

状态迁移在中枢的分配器函数( tcp_rcv_state_process )中进行,可能的代码路径包括处理现存连接的 tcp_rcv_established ,以及尚未讨论的 tcp_close 函数。

在用户进程决定调用库函数 close 关闭连接时,会调用 tcp_close 。如果套接字的状态为LISTEN(即没有到另一台计算机的连接),因为不需要通知其他参与方连接的结束。在过程开始时会检查这种情况,如果确实如此,则将套接字的状态改为CLOSED。

否则,在通过 tcp_close_state 并 tcp_set_state 调用链将套接字状态设置为FIN_WAIT_1之后,tcp_send_fin 向另一方发送一个FIN分组。

从FIN_WAIT_1到FIN_WAIT_2状态的迁移通过中枢的分配器函数 tcp_rcv_state_process 进行,因为不再需要采取快速路径处理现存连接。我们熟悉的一种情况是,收到的带有ACK标志的分组触发到FIN_WAIT_2状态的迁移,具体的状态迁移通过 tcp_set_state 进行。

现在只需要从另一方发送过来的一个FIN分组,即可将TCP连接置为TIME_WAIT状态(然后会自动切换到CLOSED状态)。

在收到第一个FIN分组因而需要被动关闭连接的另一方,状态迁移的过程是类似的。因为收到第一个FIN分组是套接字状态为ESTABLISHED,处理由 tcp_rcv_established 的低速路径进行,涉及向另一方发送一个ACK分组,并将套接字状态改为TCP_CLOSING。

下一个状态转移(到LAST_ACK)是通过调用 close 库函数(进而调用了内核的 tcp_close_state函数)进行的。此时,只需要另一方再发送一个ACK分组,即可终止连接。该分组也是通过tcp_rcv_state_process 函数处理,该函数将套接字状态改为CLOSED(通过 tcp_done ),释放套接字占用的内存空间,并最终终止连接。

应用层

对程序使用的每个套接字来说,都对应于一个socket结构和sock结构的实例。二者分别充当向下和向上的接口。

socket数据结构

<linux/net.h>
struct socket {
    socket_state        state;
    unsigned long       flags;
    const struct proto_ops  *ops;
    struct fasync_struct    *fasync_list;
    struct file     *file;
    struct sock     *sk;
    wait_queue_head_t   wait;
    short           type;
};
  • type指定协议类型的数字标识符
  • state表示套接字的连接状态,可以是以下值:
<net.h>
typedef enum {
    SS_FREE = 0,
    SS_UNCONNECTED,
    SS_CONNECTING,
    SS_CONNECTED,
    SS_DISCONNECTING
} socket_state;

这里列出的枚举值,与传输层协议在建立和关闭连接时使用的状态值毫不相关。它们表示与外界(即用户程序)相关的一般性状态。

  • file 是一个指针,指向一个伪文件的 file 实例,用于与套接字通信

socket 的定义并未绑定到具体协议。这也说明了为什么需要用 proto_ops 指针指向一个数据结构,其中包含用于处理套接字的特定于协议的函数:

<linux/net.h>
struct proto_ops {
    int     family;
    struct module   *owner;
    int     (*release)   (struct socket *sock);
    int     (*bind)      (struct socket *sock,
                      struct sockaddr *myaddr,
                      int sockaddr_len);
    int     (*connect)   (struct socket *sock,
                      struct sockaddr *vaddr,
                      int sockaddr_len, int flags);
    int     (*socketpair)(struct socket *sock1,
                      struct socket *sock2);
    int     (*accept)    (struct socket *sock,
                      struct socket *newsock, int flags);
    int     (*getname)   (struct socket *sock,
                      struct sockaddr *addr,
                      int *sockaddr_len, int peer);
    unsigned int    (*poll)      (struct file *file, struct socket *sock,
                      struct poll_table_struct *wait);
    int     (*ioctl)     (struct socket *sock, unsigned int cmd,
                      unsigned long arg);
    int     (*compat_ioctl) (struct socket *sock, unsigned int cmd,
                      unsigned long arg);
    int     (*listen)    (struct socket *sock, int len);
    int     (*shutdown)  (struct socket *sock, int flags);
    int     (*setsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, int optlen);
    int     (*getsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, int __user *optlen);
    int     (*compat_setsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, int optlen);
    int     (*compat_getsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, int __user *optlen);
    int     (*sendmsg)   (struct kiocb *iocb, struct socket *sock,
                      struct msghdr *m, size_t total_len);
    int     (*recvmsg)   (struct kiocb *iocb, struct socket *sock,
                      struct msghdr *m, size_t total_len,
                      int flags);
    int     (*mmap)      (struct file *file, struct socket *sock,
                      struct vm_area_struct * vma);
    ssize_t     (*sendpage)  (struct socket *sock, struct page *page,
                      int offset, size_t size, int flags);
};

结构中包含的 sock 指针,指向一个更为冗长的结构,包含了对内核有意义的附加的套接字管理数据。该结构包含大量成员,用于一些很微妙或很少用的特性(原始定义有100多行代码)。这里使用的是一个经过大量简化的版本。请注意,内核自身将最重要的一些成员放置到 sock_common 结构中,并将该结构的一个实例嵌入到 struct sock 开始处。下列代码片段给出了这两个结构:

<net/sock.h>
struct sock_common {
    unsigned short      skc_family;
    volatile unsigned char  skc_state;
    unsigned char       skc_reuse;
    int         skc_bound_dev_if;
    struct hlist_node   skc_node;
    struct hlist_node   skc_bind_node;
    atomic_t        skc_refcnt;
    unsigned int        skc_hash;
    struct proto        *skc_prot;
    struct net      *skc_net;
};

struct sock {
    struct sock_common  __sk_common;
...
    struct sk_buff_head sk_receive_queue;
    struct sk_buff_head sk_write_queue;
    struct timer_list   sk_timer;
    void (*sk_data_ready)(struct sock *sk, int bytes);
...
    
}

系统的各个 sock 结构实例被组织到一个协议相关的散列表中。 skc_node 用作散列表的表元,而skc_hash 表示散列值。

在发送和接收数据时,需要将数据放置在包含套接字缓冲区的等待队列上( sk_receive_queue和 sk_write_queue )。

此外,每个 sock 结构都关联了一组回调函数函数,由内核用来引起用户程序对特定事件的关注或进行状态改变。在我们给出的简化版本中,只有一个函数指针 sk_data_ready ,因为它是最重要的,而且在前几节中已经提到几次。在数据到达后,需要用户进程处理时,将调用该指针指向的函数。通常,指针的值是 sock_def_readable 。

socket结 构的 ops 成员类型为 struct proto_ops ,而 sock 的 prot 成员类型为 struct proto ,二者很容易混淆。

这两个结构中有些成员的名称相似(经常是相同的),但它们表示不同的功能。尽管这里给出的操作用于(内核端)套接字层和传输层之间的通信,而 socket 结构的 ops 成员所包含的各个函数指针则用于与系统调用通信。换句话说,它们构成了用户端和内核端套接字之间的关联。

套接字和文件

对套接字文件描述符的文件操作,可以透明地重定向到网络子系统的代码。套接字使用的file_operations 如下:

<net/socket.c>

static const struct file_operations socket_file_ops = {
    .owner =    THIS_MODULE,
    .llseek =   no_llseek,
    .aio_read = sock_aio_read,
    .aio_write =    sock_aio_write,
    .poll =     sock_poll,
    .unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_sock_ioctl,
#endif
    .mmap =     sock_mmap,
    .open =     sock_no_open,   /* special open code to disallow open via /proc */
    .release =  sock_close,
    .fasync =   sock_fasync,
    .sendpage = sock_sendpage,
    .splice_write = generic_splice_sendpage,
};

inode和套接字的关联,是通过下列辅助结构,将对应的两个结构实例分配到内存中的连续位置:

include/net/sock.h
struct socket_alloc {
struct socket socket;
    struct inode vfs_inode;
};

创建套接字

sys_socket

接收数据

sys_recvfrom

发送数据

sys_sendto

内核内部的网络通信

通信函数

内核内部的网络API。其定义基本上与用户层相同:

<linux/net.h>
extern int kernel_sendmsg(struct socket *sock, struct msghdr *msg,
                    struct kvec *vec, size_t num, size_t len);
extern int kernel_recvmsg(struct socket *sock, struct msghdr *msg,
                    struct kvec *vec, size_t num,
                    size_t len, int flags);
extern int kernel_bind(struct socket *sock, struct sockaddr *addr,
               int addrlen);
extern int kernel_listen(struct socket *sock, int backlog);
extern int kernel_accept(struct socket *sock, struct socket **newsock,
             int flags);
extern int kernel_connect(struct socket *sock, struct sockaddr *addr,
              int addrlen, int flags);
extern int kernel_getsockname(struct socket *sock, struct sockaddr *addr,
                  int *addrlen);
extern int kernel_getpeername(struct socket *sock, struct sockaddr *addr,
                  int *addrlen);
extern int kernel_getsockopt(struct socket *sock, int level, int optname,
                 char *optval, int *optlen);
extern int kernel_setsockopt(struct socket *sock, int level, int optname,
                 char *optval, int optlen);
extern int kernel_sendpage(struct socket *sock, struct page *page, int offset,
               size_t size, int flags);
extern int kernel_sock_ioctl(struct socket *sock, int cmd, unsigned long arg);
extern int kernel_sock_shutdown(struct socket *sock,
                enum sock_shutdown_cmd how);

除了 kernel_sendmsg 和 kernel_recvmsg 之外,其他接口的参数大体上都与用户层API相同,只是不再通过文件描述符来指定套接字,而直接使用了指向 struct socket 实例的指针。

netlink机制

netlink是一种基于网络的机制,允许在内核内部以及内核与用户层之间进行通信。

netlink只支持数据报信息,但提供了双向通信。另外,netlink不仅支持单播消息,也可以进行多播。类似于任何其他基于套接字的机制,netlink的工作方式是异步的。

猜你喜欢

转载自www.cnblogs.com/r1ng0/p/10728322.html