通过源码理解IGMP v1的实现(基于linux1.2.13)

时间:2022-07-25
本文章向大家介绍通过源码理解IGMP v1的实现(基于linux1.2.13),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

IGMP是组成员管理协议,我们知道一般的通信是单播的,虽然主机发出的单播报文,局域网中的每个主机都会收到,但是默认情况下,主机只会处理目的ip是自己的报文。如果我想让多个主机都可以处理我发出的报文怎么办呢?这就是IGMP做的事情。他定义了组的概念,我们可以使用多播的方式,给一个组发送报文,属于这个组的主机都可以处理这个报文。下面我们看看多播是怎么实现的。首先我们看一下网络架构。

ip地址中给多播预留了一段范围的ip。IGMP的一个多播组其实就是一个多播ip。主机记录了本主机加入的多播组信息。组播路由记录了局域网中所有多播组的信息和转发信息。IGMP的实现主要分为下面几个方面。

1 加入、离开多播组

多播是和进程(或者说socket)相关的。我们可以通过以下代码加入一个多播组。

setsockopt(fd,
           IPPROTO_IP,
           IP_ADD_MEMBERSHIP,
           &mreq, // device对应的ip和加入多播组的ip
           sizeof(mreq));

mreq的结构体定义如下

struct ip_mreq 
{
    struct in_addr imr_multiaddr;    /* IP multicast address of group */
    struct in_addr imr_interface;    /* local IP address of interface */
};

我们看一下setsockopt的实现(只列出相关部分代码)

  case IP_ADD_MEMBERSHIP: 
        {
            struct ip_mreq mreq;
            static struct options optmem;
            unsigned long route_src;
            struct rtable *rt;
            struct device *dev=NULL;
            err=verify_area(VERIFY_READ, optval, sizeof(mreq));
            memcpy_fromfs(&mreq,optval,sizeof(mreq));
             // 没有设置device则根据多播组ip选择一个device
            if(mreq.imr_interface.s_addr==INADDR_ANY) 
            {
                if((rt=ip_rt_route(mreq.imr_multiaddr.s_addr,&optmem, &route_src))!=NULL)
                {
                    dev=rt->rt_dev;
                    rt->rt_use--;
                }
            }
            else
            {
                // 根据device ip找到,找到对应的device
                for(dev = dev_base; dev; dev = dev->next)
                {
                    // 在工作状态、支持多播,ip一样
                    if((dev->flags&IFF_UP)&&(dev->flags&IFF_MULTICAST)&&
                        (dev->pa_addr==mreq.imr_interface.s_addr))
                        break;
                }
            }
            // 加入多播组
            return ip_mc_join_group(sk,dev,mreq.imr_multiaddr.s_addr);
        }

拿到加入的多播组ip和device后,调用ip_mc_join_group,在socket结构体中,有一个字段维护了该socket加入的多播组信息。


int ip_mc_join_group(struct sock *sk , struct device *dev, unsigned long addr)
{
    int unused= -1;
    int i;
    // 还没有加入过多播组
    if(sk->ip_mc_list==NULL)
    {
        if((sk->ip_mc_list=(struct ip_mc_socklist *)kmalloc(sizeof(*sk->ip_mc_list), GFP_KERNEL))==NULL)
            return -ENOMEM;
        memset(sk->ip_mc_list,'',sizeof(*sk->ip_mc_list));
    }
    // 遍历加入的多播组队列,判断是否已经加入过
    for(i=0;i<IP_MAX_MEMBERSHIPS;i++)
    {
        if(sk->ip_mc_list->multiaddr[i]==addr && sk->ip_mc_list->multidev[i]==dev)
            return -EADDRINUSE;
        if(sk->ip_mc_list->multidev[i]==NULL)
            unused=i;
    }
    // 到这说明没有加入过当前设置的多播组,则记录并且加入
    if(unused==-1)
        return -ENOBUFS;
    sk->ip_mc_list->multiaddr[unused]=addr;
    sk->ip_mc_list->multidev[unused]=dev;
    // addr为多播组ip
    ip_mc_inc_group(dev,addr);
    return 0;
}

ip_mc_join_group函数的主要逻辑是把socket想加入的多播组信息记录到socket的ip_mc_list字段中(如果还没有加入过该多播组的话)。接着调ip_mc_inc_group往下走。device层维护了主机中使用了该device的多播组信息。

static void ip_mc_inc_group(struct device *dev, unsigned long addr)
{
    struct ip_mc_list *i;
    // 遍历该设置维护的多播组队列,判断是否已经有socket加入过该多播组,是则引用数加一
    for(i=dev->ip_mc_list;i!=NULL;i=i->next)
    {
        if(i->multiaddr==addr)
        {
            i->users++;
            return;
        }
    }
    // 到这说明,还没有socket加入过当前多播组,则记录并加入
    i=(struct ip_mc_list *)kmalloc(sizeof(*i), GFP_KERNEL);
    if(!i)
        return;
    i->users=1;
    i->interface=dev;
    i->multiaddr=addr;
    i->next=dev->ip_mc_list;
    // 通过igmp通知其他方
    igmp_group_added(i);
    dev->ip_mc_list=i;
}

ip_mc_inc_group函数的主要逻辑是判断socket想要加入的多播组是不是已经存在于当前device中,如果不是则新增一个节点。继续调用igmp_group_added

static void igmp_group_added(struct ip_mc_list *im)
{
    // 初始化定时器
    igmp_init_timer(im);
    // 发送一个igmp数据包,同步多播组信息(socket加入了一个新的多播组)
    igmp_send_report(im->interface, im->multiaddr, IGMP_HOST_MEMBERSHIP_REPORT);
    // 转换多播组ip到多播mac地址,并记录到device中
    ip_mc_filter_add(im->interface, im->multiaddr);
}

我们看看igmp_send_report和ip_mc_filter_add的具体逻辑。

static void igmp_send_report(struct device *dev, unsigned long address, int type)
{
    // 申请一个skb表示一个数据包
    struct sk_buff *skb=alloc_skb(MAX_IGMP_SIZE, GFP_ATOMIC);
    int tmp;
    struct igmphdr *igh;
    // 构建ip头,ip协议头的源ip是INADDR_ANY,即随机选择一个本机的,目的ip为多播组ip(address)
    tmp=ip_build_header(skb, INADDR_ANY, address, &dev, IPPROTO_IGMP, NULL,
                skb->mem_len, 0, 1);
    // data表示所有的数据部分,tmp表示ip头大小,所以igh就是ip协议的数据部分,即igmp报文的内容
    igh=(struct igmphdr *)(skb->data+tmp);
    skb->len=tmp+sizeof(*igh);
    igh->csum=0;
    igh->unused=0;
    igh->type=type;
    igh->group=address;
    igh->csum=ip_compute_csum((void *)igh,sizeof(*igh));
    // 调用ip层发送出去
    ip_queue_xmit(NULL,dev,skb,1);
}

igmp_send_report其实就是构造一个igmp协议数据包,然后发送出去,igmp的协议格式如下

struct igmphdr
{
    // 类型
    unsigned char type;
    unsigned char unused;
    // 校验和
    unsigned short csum;
    // igmp的数据部分,比如加入多播组的时候,group表示多播组ip
    unsigned long group;
};

接着我们看ip_mc_filter_add

void ip_mc_filter_add(struct device *dev, unsigned long addr)
{
    char buf[6];
    // 把多播组ip转成mac多播地址
    addr=ntohl(addr);
    buf[0]=0x01;
    buf[1]=0x00;
    buf[2]=0x5e;
    buf[5]=addr&0xFF;
    addr>>=8;
    buf[4]=addr&0xFF;
    addr>>=8;
    buf[3]=addr&0x7F;
    dev_mc_add(dev,buf,ETH_ALEN,0);
}

我们知道ip地址是32位,mac地址是48位,但是IANA规定,ipv4组播MAC地址的高24位是0x01005E,第25位是0,低23位是ipv4组播地址的低23位。而多播的ip地址高四位固定是1110。另外低23位被映射到mac多播地址的23位,所以多播ip地址中,有5位是可以随机组合的。这就意味着,每32个多播ip地址,映射到一个mac地址。这会带来一些问题,假设主机x加入了多播组a,主机y加入了多播组b,而a和b对应的mac多播地址是一样的。当主机z给多播组a发送一个数据包的时候,这时候主机x和y的网卡都会处理该数据包,并上报到上层,但是多播组a对应的mac多播地址和多播组b是一样的。我们拿到一个多播组ip的时候,可以计算出他的多播mac地址,但是反过来就不行,因为一个多播mac地址对应了32个多播ip地址。那主机x和y怎么判断是不是发给自己的数据包?因为device维护了一个本device上的多播ip列表,操作系统根据收到的数据包中的ip目的地址和device的多播ip列表对比。如果在列表中,则说明是发给自己的。我们看看具体的实现(来自ip层收到ip数据包时的处理逻辑)。

// 是目的ip是多播ip,并且不是IGMP_ALL_HOSTS,IGMP_ALL_HOSTS是所有多播组的所有主机都可以处理的
if(brd==IS_MULTICAST && iph->daddr!=IGMP_ALL_HOSTS && !(dev->flags&IFF_LOOPBACK))
    {
        struct ip_mc_list *ip_mc=dev->ip_mc_list;
        do
        {
            // 找不到,丢包
            if(ip_mc==NULL)
            {    
                kfree_skb(skb, FREE_WRITE);
                return 0;
            }
            // 目的ip在该设置的多播ip列表中,处理该数据包
            if(ip_mc->multiaddr==iph->daddr)
                break;
            ip_mc=ip_mc->next;
        }
        while(1);
    }

最后我们看看dev_mc_add。device中维护了当前的mac多播地址列表,他会把这个列表信息同步到网卡中,使得网卡可以处理该列表中多播mac地址的数据包。

void dev_mc_add(struct device *dev, void *addr, int alen, int newonly)
{
    struct dev_mc_list *dmi;
    // device维护的多播mac地址列表
    for(dmi=dev->mc_list;dmi!=NULL;dmi=dmi->next)
    {
        // 已存在,则引用计数加一
        if(memcmp(dmi->dmi_addr,addr,dmi->dmi_addrlen)==0 && dmi->dmi_addrlen==alen)
        {
            if(!newonly)
                dmi->dmi_users++;
            return;
        }
    }
    // 不存在则新增一个项到device列表中
    dmi=(struct dev_mc_list *)kmalloc(sizeof(*dmi),GFP_KERNEL);
    memcpy(dmi->dmi_addr, addr, alen);
    dmi->dmi_addrlen=alen;
    dmi->next=dev->mc_list;
    dmi->dmi_users=1;
    dev->mc_list=dmi;
    dev->mc_count++;
    // 通知网卡需要处理该多播mac地址
    dev_mc_upload(dev);
}

网卡的工作模式有几种,分别是正常模式(只接收发给自己的数据包)、混杂模式(接收所有数据包)、多播模式(接收一般数据包和多播数据包)。网卡默认是只处理发给自己的数据包,所以当我们加入一个多播组的时候,我们需要告诉网卡,当收到该多播组的数据包时,需要处理,而不是忽略。dev_mc_upload函数就是通知网卡。

void dev_mc_upload(struct device *dev)
{
    struct dev_mc_list *dmi;
    char *data, *tmp;
    // 不工作了
    if(!(dev->flags&IFF_UP))
        return;
    // 当前是混杂模式,则不需要设置多播了,因为网卡会处理所有收到的数据,不管是不是发给自己的
    if(dev->flags&IFF_PROMISC)
    {
        dev->set_multicast_list(dev, -1, NULL);
        return;
    }
    // 多播地址个数,为0,则设置网卡工作模式为正常模式,因为不需要处理多播了
    if(dev->mc_count==0)
    {
        dev->set_multicast_list(dev,0,NULL);
        return;
    }

    data=kmalloc(dev->mc_count*dev->addr_len, GFP_KERNEL);
    // 复制所有的多播mac地址信息
    for(tmp = data, dmi=dev->mc_list;dmi!=NULL;dmi=dmi->next)
    {
        memcpy(tmp,dmi->dmi_addr, dmi->dmi_addrlen);
        tmp+=dev->addr_len;
    }
    // 告诉网卡
    dev->set_multicast_list(dev,dev->mc_count,data);
    kfree(data);
}

最后我们看一下set_multicast_list

static void
set_multicast_list(struct device *dev, int num_addrs, void *addrs)
{
    int ioaddr = dev->base_addr;
    // 多播模式
    if (num_addrs > 0) {
    outb(RX_MULT, RX_CMD);
    inb(RX_STATUS);        /* Clear status. */
    } else if (num_addrs < 0) { // 混杂模式
    outb(RX_PROM, RX_CMD);
    inb(RX_STATUS);
    } else { // 正常模式
    outb(RX_NORM, RX_CMD);
    inb(RX_STATUS);
    }
}

set_multicast_list就是设置网卡工作模式的函数。至此,我们就成功加入了一个多播组。离开一个多播组也是类似的过程。

2 维护多播组信息

加入多播组后,我们可以主动退出多播组,但是如果追主机挂了,就无法主动退出了,所以多播路由也会定期向所有多播组的所有主机发送探测报文, 2.1 监听来自多播路由的探测报文

void ip_mc_allhost(struct device *dev)
{
    struct ip_mc_list *i;
    for(i=dev->ip_mc_list;i!=NULL;i=i->next)
        if(i->multiaddr==IGMP_ALL_HOSTS)
            return;
    i=(struct ip_mc_list *)kmalloc(sizeof(*i), GFP_KERNEL);
    if(!i)
        return;
    i->users=1;
    i->interface=dev;
    i->multiaddr=IGMP_ALL_HOSTS;
    i->next=dev->ip_mc_list;
    dev->ip_mc_list=i;
    ip_mc_filter_add(i->interface, i->multiaddr);
}

设备启动的时候,操作系统会设置网卡监听目的ip是224.0.0.1的报文,使得可以处理目的ip是224.0.0.1的多播消息。该类型的报文是多播路由用于查询局域网当前多播组情况的,比如查询哪些多播组已经没有成员了,如果没有成员则删除路由信息。2.2 处理某设备的IGMP报文

int igmp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt,
    unsigned long daddr, unsigned short len, unsigned long saddr, int redo,
    struct inet_protocol *protocol)
{
    // igmp报头
    struct igmphdr *igh=(struct igmphdr *)skb->h.raw;
    // 该数据包是发给所有多播主机的,用于查询本多播组中是否还有成员
    if(igh->type==IGMP_HOST_MEMBERSHIP_QUERY && daddr==IGMP_ALL_HOSTS)
        igmp_heard_query(dev);
    // 该数据包是其他成员对多播路由查询报文的回复,同多播组的主机也会收到
    if(igh->type==IGMP_HOST_MEMBERSHIP_REPORT && daddr==igh->group)
        igmp_heard_report(dev,igh->group);
    kfree_skb(skb, FREE_READ);
    return 0;
}

IGMP v1只处理两种报文,分别是组成员查询报文(查询组是否有成员),其他成员回复多播路由的报告报文。组成员查询报文由多播路由发出,所有的多播组中的所有主机都可以收到。组成员查询报文的ip协议头的目的地址是224.0.0.1(IGMP_ALL_HOSTS),代表所有的组播主机都可以处理该报文。我们看一下这两种报文的具体实现。

static void igmp_heard_query(struct device *dev)
{
    struct ip_mc_list *im;
    for(im=dev->ip_mc_list;im!=NULL;im=im->next)
        // IGMP_ALL_HOSTS表示所有组播主机
        if(!im->tm_running && im->multiaddr!=IGMP_ALL_HOSTS)
            igmp_start_timer(im);
}

该函数用于处理组播路由的查询报文,dev->ip_mc_list是该设备对应的所有多播组信息,这里针对该设备中的每一个多播组,开启对应的定时器,超时后会发送回复报文给多播路由。我们看一下开启定时器的逻辑。

// 开启一个定时器
static void igmp_start_timer(struct ip_mc_list *im)
{
    int tv;
    if(im->tm_running)
        return;
    tv=random()%(10*HZ);        /* Pick a number any number 8) */
    im->timer.expires=tv;
    im->tm_running=1;
    add_timer(&im->timer);
}

随机选择一个超时时间,然后插入系统维护的定时器队列。为什么使用定时器,而不是立即回复呢?因为多播路由只需要知道某个多播组是否至少还有一个成员,如果有的话就保存该多播组信息,否则就删除路由项。如果某多播组在局域网中有多个成员,那么多个成员都会处理该报文,如果都立即响应,则会引起过多没有必要的流量,因为组播路由只需要收到一个响应就行。我们看看超时时的逻辑。

static void igmp_init_timer(struct ip_mc_list *im)
{
    im->tm_running=0;
    init_timer(&im->timer);
    im->timer.data=(unsigned long)im;
    im->timer.function=&igmp_timer_expire;
}

static void igmp_timer_expire(unsigned long data)
{
    struct ip_mc_list *im=(struct ip_mc_list *)data;
    igmp_stop_timer(im);
    igmp_send_report(im->interface, im->multiaddr, IGMP_HOST_MEMBERSHIP_REPORT);
}

我们看到,超时后会执行igmp_send_report发送一个类型是IGMP_HOST_MEMBERSHIP_REPORT的IGMP、目的ip是多播组ip的报文,说明该多播组还有成员。该报文不仅会发送给多播路由,还会发给同多播组的所有主机。其他主机也是类似的逻辑,即开启一个定时器。所以最快到期的主机会先发送回复报文给多播路由和同多播组的成员,我们看一下其他同多播组的主机收到该类报文时的处理逻辑


// 成员报告报文并且多播组是当前设置关联的多播组
if(igh->type==IGMP_HOST_MEMBERSHIP_REPORT && daddr==igh->group)
        igmp_heard_report(dev,igh->group);

当一个多播组的其他成员针对多播路由的查询报文作了响应,因为该响应报文的目的ip是多播组ip,所以该多播组的其他成员也能收到该报文。当某个主机收到该类型的报文的时候,就知道同多播组的其他成员已经回复了多播路由了,我们就不需要回复了。

/*
    收到其他组成员,对于多播路由查询报文的回复,则自己就不用回复了,
    因为多播路由知道该组还有成员,不会删除路由信息,减少网络流量
*/
static void igmp_heard_report(struct device *dev, unsigned long address)
{
    struct ip_mc_list *im;
    for(im=dev->ip_mc_list;im!=NULL;im=im->next)
        if(im->multiaddr==address)
            igmp_stop_timer(im);
}

我们看到,这里会删除定时器。即不会作为响应了。2.3 其他 socket关闭, 退出他之前加入过的多播


void ip_mc_drop_socket(struct sock *sk)
{
    int i;

    if(sk->ip_mc_list==NULL)
        return;

    for(i=0;i<IP_MAX_MEMBERSHIPS;i++)
    {
        if(sk->ip_mc_list->multidev[i])
        {
            ip_mc_dec_group(sk->ip_mc_list->multidev[i], sk->ip_mc_list->multiaddr[i]);
            sk->ip_mc_list->multidev[i]=NULL;
        }
    }
    kfree_s(sk->ip_mc_list,sizeof(*sk->ip_mc_list));
    sk->ip_mc_list=NULL;
}

设备停止工作了,删除对应的多播信息

void ip_mc_drop_device(struct device *dev)
{
    struct ip_mc_list *i;
    struct ip_mc_list *j;
    for(i=dev->ip_mc_list;i!=NULL;i=j)
    {
        j=i->next;
        kfree_s(i,sizeof(*i));
    }
    dev->ip_mc_list=NULL;
}

以上是IGMP v1版本的实现,在后续v2 v3版本了又增加了很多功能,比如离开组报文(linux1.2.13已经实现了),针对离开报文中的多播组,增加特定组查询报文,用于查询某个组中是否还有成员,另外还有路由选举,当局域网中有多个多播路由,多播路由之间通过协议选举出ip最小的路由为查询路由,定时给多播组发送探测报文。然后成为查询器的多播路由,会定期给其他多播路由同步心跳。否则其他多播路由会在定时器超时时认为当前查询路由已经挂了,重新选举。