Skip to content
Snippets Groups Projects
mpc.c 38.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • 			       unsigned long event, void *ptr)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    
    	struct net_device *dev = netdev_notifier_info_to_dev(ptr);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct mpoa_client *mpc;
    	struct lec_priv *priv;
    
    
    	if (!net_eq(dev_net(dev), &init_net))
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (dev->name == NULL || strncmp(dev->name, "lec", 3))
    		return NOTIFY_DONE; /* we are only interested in lec:s */
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	switch (event) {
    	case NETDEV_REGISTER:       /* a new lec device was allocated */
    
    		priv = netdev_priv(dev);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (priv->lane_version < 2)
    			break;
    		priv->lane2_ops->associate_indicator = lane2_assoc_ind;
    		mpc = find_mpc_by_itfnum(priv->itfnum);
    		if (mpc == NULL) {
    
    			dprintk("allocating new mpc for %s\n", dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			mpc = alloc_mpc();
    			if (mpc == NULL) {
    
    				pr_info("no new mpc");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    				break;
    			}
    		}
    		mpc->dev_num = priv->itfnum;
    		mpc->dev = dev;
    		dev_hold(dev);
    
    		dprintk("(%s) was initialized\n", dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		break;
    	case NETDEV_UNREGISTER:
    		/* the lec device was deallocated */
    		mpc = find_mpc_by_lec(dev);
    		if (mpc == NULL)
    			break;
    
    		dprintk("device (%s) was deallocated\n", dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		stop_mpc(mpc);
    		dev_put(mpc->dev);
    		mpc->dev = NULL;
    		break;
    	case NETDEV_UP:
    		/* the dev was ifconfig'ed up */
    		mpc = find_mpc_by_lec(dev);
    		if (mpc == NULL)
    			break;
    
    		if (mpc->mpoad_vcc != NULL)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			start_mpc(mpc, dev);
    		break;
    	case NETDEV_DOWN:
    		/* the dev was ifconfig'ed down */
    		/* this means that the flow of packets from the
    		 * upper layer stops
    		 */
    		mpc = find_mpc_by_lec(dev);
    		if (mpc == NULL)
    			break;
    
    		if (mpc->mpoad_vcc != NULL)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			stop_mpc(mpc);
    		break;
    	case NETDEV_REBOOT:
    	case NETDEV_CHANGE:
    	case NETDEV_CHANGEMTU:
    	case NETDEV_CHANGEADDR:
    	case NETDEV_GOING_DOWN:
    		break;
    	default:
    		break;
    	}
    
    	return NOTIFY_DONE;
    }
    
    /*
     * Functions which are called after a message is received from mpcd.
     * Msg is reused on purpose.
     */
    
    
    static void MPOA_trigger_rcvd(struct k_message *msg, struct mpoa_client *mpc)
    {
    
    Al Viro's avatar
    Al Viro committed
    	__be32 dst_ip = msg->content.in_info.in_dst_ip;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	in_cache_entry *entry;
    
    	entry = mpc->in_ops->get(dst_ip, mpc);
    
    	if (entry == NULL) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		entry = mpc->in_ops->add_entry(dst_ip, mpc);
    		entry->entry_state = INGRESS_RESOLVING;
    		msg->type = SND_MPOA_RES_RQST;
    		msg->content.in_info = entry->ctrl_info;
    		msg_to_mpoad(msg, mpc);
    		do_gettimeofday(&(entry->reply_wait));
    		mpc->in_ops->put(entry);
    		return;
    	}
    
    	if (entry->entry_state == INGRESS_INVALID) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		entry->entry_state = INGRESS_RESOLVING;
    		msg->type = SND_MPOA_RES_RQST;
    		msg->content.in_info = entry->ctrl_info;
    		msg_to_mpoad(msg, mpc);
    		do_gettimeofday(&(entry->reply_wait));
    		mpc->in_ops->put(entry);
    		return;
    	}
    
    	pr_info("(%s) entry already in resolving state\n",
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		(mpc->dev) ? mpc->dev->name : "<unknown>");
    	mpc->in_ops->put(entry);
    }
    
    /*
     * Things get complicated because we have to check if there's an egress
    
     * shortcut with suitable traffic parameters we could use.
    
    Linus Torvalds's avatar
    Linus Torvalds committed
     */
    
    static void check_qos_and_open_shortcut(struct k_message *msg,
    					struct mpoa_client *client,
    					in_cache_entry *entry)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    
    Al Viro's avatar
    Al Viro committed
    	__be32 dst_ip = msg->content.in_info.in_dst_ip;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct atm_mpoa_qos *qos = atm_mpoa_search_qos(dst_ip);
    	eg_cache_entry *eg_entry = client->eg_ops->get_by_src_ip(dst_ip, client);
    
    
    	if (eg_entry && eg_entry->shortcut) {
    		if (eg_entry->shortcut->qos.txtp.traffic_class &
    		    msg->qos.txtp.traffic_class &
    		    (qos ? qos->qos.txtp.traffic_class : ATM_UBR | ATM_CBR)) {
    			if (eg_entry->shortcut->qos.txtp.traffic_class == ATM_UBR)
    				entry->shortcut = eg_entry->shortcut;
    			else if (eg_entry->shortcut->qos.txtp.max_pcr > 0)
    				entry->shortcut = eg_entry->shortcut;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		}
    
    		if (entry->shortcut) {
    
    			dprintk("(%s) using egress SVC to reach %pI4\n",
    
    				client->dev->name, &dst_ip);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			client->eg_ops->put(eg_entry);
    			return;
    		}
    	}
    	if (eg_entry != NULL)
    		client->eg_ops->put(eg_entry);
    
    	/* No luck in the egress cache we must open an ingress SVC */
    	msg->type = OPEN_INGRESS_SVC;
    
    	if (qos &&
    	    (qos->qos.txtp.traffic_class == msg->qos.txtp.traffic_class)) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		msg->qos = qos->qos;
    
    		pr_info("(%s) trying to get a CBR shortcut\n",
    			client->dev->name);
    	} else
    		memset(&msg->qos, 0, sizeof(struct atm_qos));
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	msg_to_mpoad(msg, client);
    }
    
    static void MPOA_res_reply_rcvd(struct k_message *msg, struct mpoa_client *mpc)
    {
    
    Al Viro's avatar
    Al Viro committed
    	__be32 dst_ip = msg->content.in_info.in_dst_ip;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	in_cache_entry *entry = mpc->in_ops->get(dst_ip, mpc);
    
    	dprintk("(%s) ip %pI4\n",
    
    		mpc->dev->name, &dst_ip);
    
    	ddprintk("(%s) entry = %p",
    
    		 mpc->dev->name, entry);
    	if (entry == NULL) {
    		pr_info("(%s) ARGH, received res. reply for an entry that doesn't exist.\n",
    			mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	ddprintk_cont(" entry_state = %d ", entry->entry_state);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	if (entry->entry_state == INGRESS_RESOLVED) {
    
    		pr_info("(%s) RESOLVED entry!\n", mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->in_ops->put(entry);
    		return;
    	}
    
    	entry->ctrl_info = msg->content.in_info;
    	do_gettimeofday(&(entry->tv));
    	do_gettimeofday(&(entry->reply_wait)); /* Used in refreshing func from now on */
    	entry->refresh_time = 0;
    
    	ddprintk_cont("entry->shortcut = %p\n", entry->shortcut);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    
    	if (entry->entry_state == INGRESS_RESOLVING &&
    	    entry->shortcut != NULL) {
    
    		entry->entry_state = INGRESS_RESOLVED;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->in_ops->put(entry);
    		return; /* Shortcut already open... */
    	}
    
    	if (entry->shortcut != NULL) {
    
    		pr_info("(%s) entry->shortcut != NULL, impossible!\n",
    			mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->in_ops->put(entry);
    		return;
    	}
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	check_qos_and_open_shortcut(msg, mpc, entry);
    	entry->entry_state = INGRESS_RESOLVED;
    	mpc->in_ops->put(entry);
    
    	return;
    
    }
    
    static void ingress_purge_rcvd(struct k_message *msg, struct mpoa_client *mpc)
    {
    
    Al Viro's avatar
    Al Viro committed
    	__be32 dst_ip = msg->content.in_info.in_dst_ip;
    	__be32 mask = msg->ip_mask;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	in_cache_entry *entry = mpc->in_ops->get_with_mask(dst_ip, mpc, mask);
    
    
    	if (entry == NULL) {
    		pr_info("(%s) purge for a non-existing entry, ip = %pI4\n",
    			mpc->dev->name, &dst_ip);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	do {
    
    		dprintk("(%s) removing an ingress entry, ip = %pI4\n",
    
    			mpc->dev->name, &dst_ip);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		write_lock_bh(&mpc->ingress_lock);
    		mpc->in_ops->remove_entry(entry, mpc);
    		write_unlock_bh(&mpc->ingress_lock);
    		mpc->in_ops->put(entry);
    		entry = mpc->in_ops->get_with_mask(dst_ip, mpc, mask);
    	} while (entry != NULL);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    static void egress_purge_rcvd(struct k_message *msg, struct mpoa_client *mpc)
    {
    
    Al Viro's avatar
    Al Viro committed
    	__be32 cache_id = msg->content.eg_info.cache_id;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	eg_cache_entry *entry = mpc->eg_ops->get_by_cache_id(cache_id, mpc);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (entry == NULL) {
    
    		dprintk("(%s) purge for a non-existing entry\n",
    
    			mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	write_lock_irq(&mpc->egress_lock);
    	mpc->eg_ops->remove_entry(entry, mpc);
    	write_unlock_irq(&mpc->egress_lock);
    
    	mpc->eg_ops->put(entry);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    static void purge_egress_shortcut(struct atm_vcc *vcc, eg_cache_entry *entry)
    {
    	struct sock *sk;
    	struct k_message *purge_msg;
    	struct sk_buff *skb;
    
    
    	dprintk("entering\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (vcc == NULL) {
    
    		pr_info("vcc == NULL\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	skb = alloc_skb(sizeof(struct k_message), GFP_ATOMIC);
    	if (skb == NULL) {
    
    		pr_info("out of memory\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	skb_put(skb, sizeof(struct k_message));
    	memset(skb->data, 0, sizeof(struct k_message));
    	purge_msg = (struct k_message *)skb->data;
    	purge_msg->type = DATA_PLANE_PURGE;
    	if (entry != NULL)
    		purge_msg->content.eg_info = entry->ctrl_info;
    
    	atm_force_charge(vcc, skb->truesize);
    
    	sk = sk_atm(vcc);
    	skb_queue_tail(&sk->sk_receive_queue, skb);
    
    	dprintk("exiting\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    }
    
    /*
     * Our MPS died. Tell our daemon to send NHRP data plane purge to each
     * of the egress shortcuts we have.
     */
    
    static void mps_death(struct k_message *msg, struct mpoa_client *mpc)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    	eg_cache_entry *entry;
    
    
    	dprintk("(%s)\n", mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    
    	if (memcmp(msg->MPS_ctrl, mpc->mps_ctrl_addr, ATM_ESA_LEN)) {
    		pr_info("(%s) wrong MPS\n", mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    
    	/* FIXME: This knows too much of the cache structure */
    	read_lock_irq(&mpc->egress_lock);
    	entry = mpc->eg_cache;
    	while (entry != NULL) {
    		purge_egress_shortcut(entry->shortcut, entry);
    		entry = entry->next;
    	}
    	read_unlock_irq(&mpc->egress_lock);
    
    	mpc->in_ops->destroy_cache(mpc);
    	mpc->eg_ops->destroy_cache(mpc);
    }
    
    
    static void MPOA_cache_impos_rcvd(struct k_message *msg,
    				  struct mpoa_client *mpc)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    	uint16_t holding_time;
    	eg_cache_entry *entry = mpc->eg_ops->get_by_cache_id(msg->content.eg_info.cache_id, mpc);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	holding_time = msg->content.eg_info.holding_time;
    
    	dprintk("(%s) entry = %p, holding_time = %u\n",
    
    		mpc->dev->name, entry, holding_time);
    	if (entry == NULL && holding_time) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		entry = mpc->eg_ops->add_entry(msg, mpc);
    		mpc->eg_ops->put(entry);
    		return;
    	}
    
    	if (holding_time) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->eg_ops->update(entry, holding_time);
    		return;
    	}
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	write_lock_irq(&mpc->egress_lock);
    	mpc->eg_ops->remove_entry(entry, mpc);
    	write_unlock_irq(&mpc->egress_lock);
    
    	mpc->eg_ops->put(entry);
    }
    
    
    static void set_mpc_ctrl_addr_rcvd(struct k_message *mesg,
    				   struct mpoa_client *mpc)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    	struct lec_priv *priv;
    	int i, retval ;
    
    	uint8_t tlv[4 + 1 + 1 + 1 + ATM_ESA_LEN];
    
    	tlv[0] = 00; tlv[1] = 0xa0; tlv[2] = 0x3e; tlv[3] = 0x2a; /* type  */
    	tlv[4] = 1 + 1 + ATM_ESA_LEN;  /* length                           */
    	tlv[5] = 0x02;                 /* MPOA client                      */
    	tlv[6] = 0x00;                 /* number of MPS MAC addresses      */
    
    	memcpy(&tlv[7], mesg->MPS_ctrl, ATM_ESA_LEN); /* MPC ctrl ATM addr */
    	memcpy(mpc->our_ctrl_addr, mesg->MPS_ctrl, ATM_ESA_LEN);
    
    
    	dprintk("(%s) setting MPC ctrl ATM address to",
    		mpc->dev ? mpc->dev->name : "<unknown>");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	for (i = 7; i < sizeof(tlv); i++)
    
    		dprintk_cont(" %02x", tlv[i]);
    	dprintk_cont("\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	if (mpc->dev) {
    
    		priv = netdev_priv(mpc->dev);
    
    		retval = priv->lane2_ops->associate_req(mpc->dev,
    							mpc->dev->dev_addr,
    							tlv, sizeof(tlv));
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (retval == 0)
    
    			pr_info("(%s) MPOA device type TLV association failed\n",
    				mpc->dev->name);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		retval = priv->lane2_ops->resolve(mpc->dev, NULL, 1, NULL, NULL);
    		if (retval < 0)
    
    			pr_info("(%s) targetless LE_ARP request failed\n",
    				mpc->dev->name);
    
    static void set_mps_mac_addr_rcvd(struct k_message *msg,
    				  struct mpoa_client *client)
    
    	if (client->number_of_mps_macs)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		kfree(client->mps_macs);
    	client->number_of_mps_macs = 0;
    
    	client->mps_macs = kmemdup(msg->MPS_ctrl, ETH_ALEN, GFP_KERNEL);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (client->mps_macs == NULL) {
    
    		pr_info("out of memory\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		return;
    	}
    	client->number_of_mps_macs = 1;
    }
    
    /*
     * purge egress cache and tell daemon to 'action' (DIE, RELOAD)
     */
    static void clean_up(struct k_message *msg, struct mpoa_client *mpc, int action)
    {
    
    	eg_cache_entry *entry;
    	msg->type = SND_EGRESS_PURGE;
    
    
    	/* FIXME: This knows too much of the cache structure */
    	read_lock_irq(&mpc->egress_lock);
    	entry = mpc->eg_cache;
    
    	while (entry != NULL) {
    		msg->content.eg_info = entry->ctrl_info;
    
    		dprintk("cache_id %u\n", entry->ctrl_info.cache_id);
    
    		msg_to_mpoad(msg, mpc);
    		entry = entry->next;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	}
    	read_unlock_irq(&mpc->egress_lock);
    
    	msg->type = action;
    	msg_to_mpoad(msg, mpc);
    }
    
    static void mpc_timer_refresh(void)
    {
    	mpc_timer.expires = jiffies + (MPC_P2 * HZ);
    	mpc_timer.data = mpc_timer.expires;
    	mpc_timer.function = mpc_cache_check;
    	add_timer(&mpc_timer);
    }
    
    
    static void mpc_cache_check(unsigned long checking_time)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    	struct mpoa_client *mpc = mpcs;
    	static unsigned long previous_resolving_check_time;
    	static unsigned long previous_refresh_time;
    
    	while (mpc != NULL) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->in_ops->clear_count(mpc);
    		mpc->eg_ops->clear_expired(mpc);
    
    		if (checking_time - previous_resolving_check_time >
    		    mpc->parameters.mpc_p4 * HZ) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			mpc->in_ops->check_resolving(mpc);
    			previous_resolving_check_time = checking_time;
    		}
    
    		if (checking_time - previous_refresh_time >
    		    mpc->parameters.mpc_p5 * HZ) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			mpc->in_ops->refresh(mpc);
    			previous_refresh_time = checking_time;
    		}
    		mpc = mpc->next;
    	}
    	mpc_timer_refresh();
    }
    
    
    static int atm_mpoa_ioctl(struct socket *sock, unsigned int cmd,
    			  unsigned long arg)
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    {
    	int err = 0;
    	struct atm_vcc *vcc = ATM_SD(sock);
    
    	if (cmd != ATMMPC_CTRL && cmd != ATMMPC_DATA)
    		return -ENOIOCTLCMD;
    
    	if (!capable(CAP_NET_ADMIN))
    		return -EPERM;
    
    	switch (cmd) {
    
    	case ATMMPC_CTRL:
    		err = atm_mpoa_mpoad_attach(vcc, (int)arg);
    		if (err >= 0)
    			sock->state = SS_CONNECTED;
    		break;
    	case ATMMPC_DATA:
    		err = atm_mpoa_vcc_attach(vcc, (void __user *)arg);
    		break;
    	default:
    		break;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	}
    	return err;
    }
    
    static struct atm_ioctl atm_ioctl_ops = {
    	.owner	= THIS_MODULE,
    	.ioctl	= atm_mpoa_ioctl,
    };
    
    static __init int atm_mpoa_init(void)
    {
    	register_atm_ioctl(&atm_ioctl_ops);
    
    	if (mpc_proc_init() != 0)
    
    		pr_info("failed to initialize /proc/mpoa\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    
    Michal Marek's avatar
    Michal Marek committed
    	pr_info("mpc.c: initialized\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	return 0;
    }
    
    static void __exit atm_mpoa_cleanup(void)
    {
    	struct mpoa_client *mpc, *tmp;
    	struct atm_mpoa_qos *qos, *nextqos;
    	struct lec_priv *priv;
    
    	mpc_proc_clean();
    
    
    	del_timer_sync(&mpc_timer);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	unregister_netdevice_notifier(&mpoa_notifier);
    	deregister_atm_ioctl(&atm_ioctl_ops);
    
    	mpc = mpcs;
    	mpcs = NULL;
    	while (mpc != NULL) {
    		tmp = mpc->next;
    		if (mpc->dev != NULL) {
    			stop_mpc(mpc);
    
    			priv = netdev_priv(mpc->dev);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			if (priv->lane2_ops != NULL)
    				priv->lane2_ops->associate_indicator = NULL;
    		}
    
    		ddprintk("about to clear caches\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc->in_ops->destroy_cache(mpc);
    		mpc->eg_ops->destroy_cache(mpc);
    
    		ddprintk("caches cleared\n");
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		kfree(mpc->mps_macs);
    		memset(mpc, 0, sizeof(struct mpoa_client));
    
    		ddprintk("about to kfree %p\n", mpc);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		kfree(mpc);
    
    		ddprintk("next mpc is at %p\n", tmp);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		mpc = tmp;
    	}
    
    	qos = qos_head;
    	qos_head = NULL;
    	while (qos != NULL) {
    		nextqos = qos->next;
    
    		dprintk("freeing qos entry %p\n", qos);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		kfree(qos);
    		qos = nextqos;
    	}
    }
    
    module_init(atm_mpoa_init);
    module_exit(atm_mpoa_cleanup);
    
    MODULE_LICENSE("GPL");