| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * net/sched/sch_fifo.c The simplest FIFO queue. | |
| * | |
| * Authors: Alexey Kuznetsov, <kuznet@ms2.inr.ac.ru> | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/slab.h> | |
| #include <linux/types.h> | |
| #include <linux/kernel.h> | |
| #include <linux/errno.h> | |
| #include <linux/skbuff.h> | |
| #include <net/pkt_sched.h> | |
| #include <net/pkt_cls.h> | |
| /* 1 band FIFO pseudo-"scheduler" */ | |
| static int bfifo_enqueue(struct sk_buff *skb, struct Qdisc *sch, | |
| struct sk_buff **to_free) | |
| { | |
| if (likely((u64)sch->qstats.backlog + qdisc_pkt_len(skb) <= | |
| READ_ONCE(sch->limit))) | |
| return qdisc_enqueue_tail(skb, sch); | |
| return qdisc_drop(skb, sch, to_free); | |
| } | |
| static int pfifo_enqueue(struct sk_buff *skb, struct Qdisc *sch, | |
| struct sk_buff **to_free) | |
| { | |
| if (likely(sch->q.qlen < READ_ONCE(sch->limit))) | |
| return qdisc_enqueue_tail(skb, sch); | |
| return qdisc_drop(skb, sch, to_free); | |
| } | |
| static int pfifo_tail_enqueue(struct sk_buff *skb, struct Qdisc *sch, | |
| struct sk_buff **to_free) | |
| { | |
| unsigned int prev_backlog; | |
| if (unlikely(READ_ONCE(sch->limit) == 0)) | |
| return qdisc_drop(skb, sch, to_free); | |
| if (likely(sch->q.qlen < READ_ONCE(sch->limit))) | |
| return qdisc_enqueue_tail(skb, sch); | |
| prev_backlog = sch->qstats.backlog; | |
| /* queue full, remove one skb to fulfill the limit */ | |
| __qdisc_queue_drop_head(sch, &sch->q, to_free); | |
| qdisc_qstats_drop(sch); | |
| qdisc_enqueue_tail(skb, sch); | |
| qdisc_tree_reduce_backlog(sch, 0, prev_backlog - sch->qstats.backlog); | |
| return NET_XMIT_CN; | |
| } | |
| static void fifo_offload_init(struct Qdisc *sch) | |
| { | |
| struct net_device *dev = qdisc_dev(sch); | |
| struct tc_fifo_qopt_offload qopt; | |
| if (!tc_can_offload(dev) || !dev->netdev_ops->ndo_setup_tc) | |
| return; | |
| qopt.command = TC_FIFO_REPLACE; | |
| qopt.handle = sch->handle; | |
| qopt.parent = sch->parent; | |
| dev->netdev_ops->ndo_setup_tc(dev, TC_SETUP_QDISC_FIFO, &qopt); | |
| } | |
| static void fifo_offload_destroy(struct Qdisc *sch) | |
| { | |
| struct net_device *dev = qdisc_dev(sch); | |
| struct tc_fifo_qopt_offload qopt; | |
| if (!tc_can_offload(dev) || !dev->netdev_ops->ndo_setup_tc) | |
| return; | |
| qopt.command = TC_FIFO_DESTROY; | |
| qopt.handle = sch->handle; | |
| qopt.parent = sch->parent; | |
| dev->netdev_ops->ndo_setup_tc(dev, TC_SETUP_QDISC_FIFO, &qopt); | |
| } | |
| static int fifo_offload_dump(struct Qdisc *sch) | |
| { | |
| struct tc_fifo_qopt_offload qopt; | |
| qopt.command = TC_FIFO_STATS; | |
| qopt.handle = sch->handle; | |
| qopt.parent = sch->parent; | |
| qopt.stats.bstats = &sch->bstats; | |
| qopt.stats.qstats = &sch->qstats; | |
| return qdisc_offload_dump_helper(sch, TC_SETUP_QDISC_FIFO, &qopt); | |
| } | |
| static int __fifo_init(struct Qdisc *sch, struct nlattr *opt, | |
| struct netlink_ext_ack *extack) | |
| { | |
| bool bypass; | |
| bool is_bfifo = sch->ops == &bfifo_qdisc_ops; | |
| if (opt == NULL) { | |
| u32 limit = qdisc_dev(sch)->tx_queue_len; | |
| if (is_bfifo) | |
| limit *= psched_mtu(qdisc_dev(sch)); | |
| WRITE_ONCE(sch->limit, limit); | |
| } else { | |
| struct tc_fifo_qopt *ctl = nla_data(opt); | |
| if (nla_len(opt) < sizeof(*ctl)) | |
| return -EINVAL; | |
| WRITE_ONCE(sch->limit, ctl->limit); | |
| } | |
| if (is_bfifo) | |
| bypass = sch->limit >= psched_mtu(qdisc_dev(sch)); | |
| else | |
| bypass = sch->limit >= 1; | |
| if (bypass) | |
| sch->flags |= TCQ_F_CAN_BYPASS; | |
| else | |
| sch->flags &= ~TCQ_F_CAN_BYPASS; | |
| return 0; | |
| } | |
| static int fifo_init(struct Qdisc *sch, struct nlattr *opt, | |
| struct netlink_ext_ack *extack) | |
| { | |
| int err; | |
| err = __fifo_init(sch, opt, extack); | |
| if (err) | |
| return err; | |
| fifo_offload_init(sch); | |
| return 0; | |
| } | |
| static int fifo_hd_init(struct Qdisc *sch, struct nlattr *opt, | |
| struct netlink_ext_ack *extack) | |
| { | |
| return __fifo_init(sch, opt, extack); | |
| } | |
| static void fifo_destroy(struct Qdisc *sch) | |
| { | |
| fifo_offload_destroy(sch); | |
| } | |
| static int __fifo_dump(struct Qdisc *sch, struct sk_buff *skb) | |
| { | |
| struct tc_fifo_qopt opt = { .limit = READ_ONCE(sch->limit) }; | |
| if (nla_put(skb, TCA_OPTIONS, sizeof(opt), &opt)) | |
| goto nla_put_failure; | |
| return skb->len; | |
| nla_put_failure: | |
| return -1; | |
| } | |
| static int fifo_dump(struct Qdisc *sch, struct sk_buff *skb) | |
| { | |
| int err; | |
| err = fifo_offload_dump(sch); | |
| if (err) | |
| return err; | |
| return __fifo_dump(sch, skb); | |
| } | |
| static int fifo_hd_dump(struct Qdisc *sch, struct sk_buff *skb) | |
| { | |
| return __fifo_dump(sch, skb); | |
| } | |
| struct Qdisc_ops pfifo_qdisc_ops __read_mostly = { | |
| .id = "pfifo", | |
| .priv_size = 0, | |
| .enqueue = pfifo_enqueue, | |
| .dequeue = qdisc_dequeue_head, | |
| .peek = qdisc_peek_head, | |
| .init = fifo_init, | |
| .destroy = fifo_destroy, | |
| .reset = qdisc_reset_queue, | |
| .change = fifo_init, | |
| .dump = fifo_dump, | |
| .owner = THIS_MODULE, | |
| }; | |
| EXPORT_SYMBOL(pfifo_qdisc_ops); | |
| struct Qdisc_ops bfifo_qdisc_ops __read_mostly = { | |
| .id = "bfifo", | |
| .priv_size = 0, | |
| .enqueue = bfifo_enqueue, | |
| .dequeue = qdisc_dequeue_head, | |
| .peek = qdisc_peek_head, | |
| .init = fifo_init, | |
| .destroy = fifo_destroy, | |
| .reset = qdisc_reset_queue, | |
| .change = fifo_init, | |
| .dump = fifo_dump, | |
| .owner = THIS_MODULE, | |
| }; | |
| EXPORT_SYMBOL(bfifo_qdisc_ops); | |
| struct Qdisc_ops pfifo_head_drop_qdisc_ops __read_mostly = { | |
| .id = "pfifo_head_drop", | |
| .priv_size = 0, | |
| .enqueue = pfifo_tail_enqueue, | |
| .dequeue = qdisc_dequeue_head, | |
| .peek = qdisc_peek_head, | |
| .init = fifo_hd_init, | |
| .reset = qdisc_reset_queue, | |
| .change = fifo_hd_init, | |
| .dump = fifo_hd_dump, | |
| .owner = THIS_MODULE, | |
| }; | |
| /* Pass size change message down to embedded FIFO */ | |
| int fifo_set_limit(struct Qdisc *q, unsigned int limit) | |
| { | |
| struct nlattr *nla; | |
| int ret = -ENOMEM; | |
| /* Hack to avoid sending change message to non-FIFO */ | |
| if (strncmp(q->ops->id + 1, "fifo", 4) != 0) | |
| return 0; | |
| if (!q->ops->change) | |
| return 0; | |
| nla = kmalloc(nla_attr_size(sizeof(struct tc_fifo_qopt)), GFP_KERNEL); | |
| if (nla) { | |
| nla->nla_type = RTM_NEWQDISC; | |
| nla->nla_len = nla_attr_size(sizeof(struct tc_fifo_qopt)); | |
| ((struct tc_fifo_qopt *)nla_data(nla))->limit = limit; | |
| ret = q->ops->change(q, nla, NULL); | |
| kfree(nla); | |
| } | |
| return ret; | |
| } | |
| EXPORT_SYMBOL(fifo_set_limit); | |
| struct Qdisc *fifo_create_dflt(struct Qdisc *sch, struct Qdisc_ops *ops, | |
| unsigned int limit, | |
| struct netlink_ext_ack *extack) | |
| { | |
| struct Qdisc *q; | |
| int err = -ENOMEM; | |
| q = qdisc_create_dflt(sch->dev_queue, ops, TC_H_MAKE(sch->handle, 1), | |
| extack); | |
| if (q) { | |
| err = fifo_set_limit(q, limit); | |
| if (err < 0) { | |
| qdisc_put(q); | |
| q = NULL; | |
| } | |
| } | |
| return q ? : ERR_PTR(err); | |
| } | |
| EXPORT_SYMBOL(fifo_create_dflt); | |
| MODULE_DESCRIPTION("Single queue packet and byte based First In First Out(P/BFIFO) scheduler"); |