Linux - QOS实现框架分析
更新时间:2023-03-11 13:00:01 阅读量: 教育文库 文档下载
- Linux常用命令推荐度:
- 相关推荐
Linux QOS实现框架分析
作者:徐晓东(joyxxd@163.com)
Linux中的QOS分为入口(Ingress)部分和出口(Egress)部分,入口部分主要用于进行入口流量限速(policing),出口部分的QOS用于队列调度(queuing scheduling)。
以下分析所参考的linux内核版本为2.6.21。 1. Ingress QOS
Ingress QOS在内核的入口点有两个,但是不能同时启用,这取决于内核编译选项。当打开了
CONFIG_NET_CLS_ACT时,入口点在src/net/core/dev.c的netif_receive_skb函数中,代码片段如下: #ifdef CONFIG_NET_CLS_ACT if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL; /* noone else should process this after*/ } else {
skb->tc_verd = SET_TC_OK2MUNGE(skb->tc_verd); }
ret = ing_filter(skb);
if (ret == TC_ACT_SHOT || (ret == TC_ACT_STOLEN)) { kfree_skb(skb); goto out; }
skb->tc_verd = 0; ncls: #endif
进入ing_filter后,代码片段如下: if ((q = dev->qdisc_ingress) != NULL) result = q->enqueue(skb, q);
我们可以看到,在这里判断了是否在设备上配置了ingress调度规则,如果配置了则调用其enqueue函数进行处理。在这里实际上调用的是src/net/core/sched/sch_ingress.c中的ingress_enqueue函数。
当没有打开CONFIG_NET_CLS_ACT,而是打开了CONFIG_NET_CLS_POLICE和CONFIG_NETFILTER时,就会在netfilter的PREROUTING钩子点处调用ing_hook函数,该函数的代码片段如下: if (dev->qdisc_ingress) {
spin_lock(&dev->queue_lock);
if ((q = dev->qdisc_ingress) != NULL) fwres = q->enqueue(skb, q); spin_unlock(&dev->queue_lock); }
可以看出与ing_filter的处理部分类似,最终都是调用了ingress qdisc的enqueue函数(即ingress_enqueue函数)。 ing_hook函数是在src/net/sched/sch_ingress.c文件中进行注册的,在sch_ingress文件中定义了一个结构实例ingress_qdisc_ops,如下所示: static struct Qdisc_ops ingress_qdisc_ops = { .next = NULL,
.cl_ops = &ingress_class_ops, .id = \
.priv_size = sizeof(struct ingress_qdisc_data), .enqueue = ingress_enqueue, .dequeue = ingress_dequeue, .requeue = ingress_requeue, .drop = ingress_drop, .init = ingress_init, .reset = ingress_reset, .destroy = ingress_destroy, .change = NULL,
.dump = ingress_dump, .owner = THIS_MODULE, };
所有的qdisc都会有这样的一个对象实例,在模块初始化时会调用register_qdisc函数将自己的struct Qdisc_ops结构实例注册到链表中,该链表头是qdisc_base,定义在sch_api.c文件中,static struct Qdisc_ops *qdisc_base。 当通过tc qdisc命令配置了ingress qdisc规则时,会调用到ingress_init函数,进入ingress_init函数代码片段如下:
#ifndef CONFIG_NET_CLS_ACT #ifdef CONFIG_NETFILTER if (!nf_registered) {
if (nf_register_hook(&ing_ops) < 0) {
printk(\ return -EINVAL; }
nf_registered++;
if (nf_register_hook(&ing6_ops) < 0) {
printk(\ \ } else
nf_registered++; }
#endif #endif
我们看到当没有定义CONFIG_NET_CLS_ACT,但是定义了CONFIG_NETFILTER时,会调用
nf_register_hook函数注册ing_ops和ing6_ops结构实例,ing_ops和ing6_ops的定义如下: static struct nf_hook_ops ing_ops = { .hook = ing_hook, .owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING, .priority = NF_IP_PRI_FILTER + 1, };
static struct nf_hook_ops ing6_ops = { .hook = ing_hook, .owner = THIS_MODULE,
.pf = PF_INET6,
.hooknum = NF_IP6_PRE_ROUTING, .priority = NF_IP6_PRI_FILTER + 1, };
针对IPV4和IPV6协议,注册的hook函数都是ing_hook,hook点是在PREROUTING,优先级低于NF_IP_PRI_FILTER。
当前比较推荐第一种使用方法,即打开CONFIG_NET_CLS_ACT选项,在netif_receive_skb函数中进入ingress的处理流程。
下面我们再跟踪一下ingress的enqueue处理流程,进入ingress_enqueue函数,代码片段如下: result = tc_classify(skb, p->filter_list, &res); ……
在ingress_enqueue函数中最主要的就是调用tc_classify函数进行分类操作,进入tc_classify函数,代码片段如下:
for ( ; tp; tp = tp->next) {
if ((tp->protocol == protocol ||
tp->protocol == __constant_htons(ETH_P_ALL)) && (err = tp->classify(skb, tp, res)) >= 0) { ……
其中tp是一个指向struct tcf_proto类型的指针,struct tcf_proto中包含了与filter相关的参数和函数指针。tp->classify调用了与该filter规则相相关联的classify函数,如果我们使用的是FW类型的filter,那么对应的classify函数就是fw_classify,该函数定义在src/net/sched/cls_fw.c文
件中。以FW分类器为例,我们再进入fw_classify函数,代码片段如下: struct fw_head *head = (struct fw_head*)tp->root; struct fw_filter *f; int r;
u32 id = skb->mark;
if (head != NULL) { id &= head->mask;
for (f=head->ht[fw_hash(id)]; f; f=f->next) { if (f->id == id) { *res = f->res;
#ifdef CONFIG_NET_CLS_IND
if (!tcf_match_indev(skb, f->indev)) continue;
#endif /* CONFIG_NET_CLS_IND */
r = tcf_exts_exec(skb, &f->exts, res); if (r < 0) continue;
return r; } }
} else {
/* old method */
if (id && (TC_H_MAJ(id) == 0 || !(TC_H_MAJ(id^tp->q->handle)))) { res->classid = id; res->class = 0; return 0; } }
return -1;
如上面代码所示,当filter中有规则时,遍历规则表,寻找与skb->mark(由ebtables或ip(6)tables来配置)相匹配的表项,如果找到了则会进一步调用tcf_exts_exec函数对扩展的action进行处理。再进入tcf_exts_exec函数,代码片段如下: #ifdef CONFIG_NET_CLS_ACT if (exts->action)
return tcf_action_exec(skb, exts->action, res); #elif defined CONFIG_NET_CLS_POLICE if (exts->police)
return tcf_police(skb, exts->police); #endif
如上面代码所示,当定义了CONFIG_NET_CLS_ACT选项,并且存在扩展action时调用tcf_action_exec函数进行处理,当定义了CONFIG_NET_CLS_POLICE选项,并且存在police扩展时会调用tcf_police函数。实际上CONFIG_NET_CLS_POLICE是一套老的police机制,它假定了在ingress处理
中只能进行police操作。CONFIG_NET_CLS_ACT是一套新的机制,该机制称为action扩展,即在filter规则处理之后,进一步进行额外的action处理,而Traffic Policing就属于其中的一种,它对应的文件是src/net/sched/act_police.c。如果我们通过tc filter命令配置fw的规则时用action选项指定了police作为扩展action,那么调用tcf_action_exec函数后就会最终调用到tcf_police函数。下面举个例子说明扩展action的配置方法: tc filter add dev eth0 parent ffff: protocol all prio 1 handle 100 fw action police rate 1000Kbit burst 2000k drop
从上面的命令可以看出,直接在fw后面加上action关键字即可配置action扩展。
下面我们再看看ingress qdisc是如何配置到接口上的,在这里只关注内核的处理流程,不关注tc工具与内核如何进行通信。通过tc命令进行配置后,会通过netlink接口调用内核中已经注册的一系列配置函数。
tc qdisc和tc clsss配置命令对应的配置函数在src/net/sched/sch_api.c的pktsched_init函数中进行了初始化注册,该函数在linux系统初始化的时候会被调用到。代码片断如下: if (link_p) {
link_p[RTM_NEWQDISC-RTM_BASE].doit = tc_modify_qdisc; link_p[RTM_DELQDISC-RTM_BASE].doit = tc_get_qdisc; link_p[RTM_GETQDISC-RTM_BASE].doit = tc_get_qdisc; link_p[RTM_GETQDISC-RTM_BASE].dumpit = tc_dump_qdisc; link_p[RTM_NEWTCLASS-RTM_BASE].doit = tc_ctl_tclass; link_p[RTM_DELTCLASS-RTM_BASE].doit = tc_ctl_tclass; link_p[RTM_GETTCLASS-RTM_BASE].doit = tc_ctl_tclass; link_p[RTM_GETTCLASS-RTM_BASE].dumpit = tc_dump_tclass; }
通过以上注册的一系列函数,就可以完成tc qdisc和class的命令配置。
tc filter配置命令对应的配置函数在src/net/sched/cls_api.c的tc_filter_init函数中进行了初始化注册,该函数也会在系统初始化的时候被调用到。代码片段如下: if (link_p) {
link_p[RTM_NEWTFILTER-RTM_BASE].doit = tc_ctl_tfilter; link_p[RTM_DELTFILTER-RTM_BASE].doit = tc_ctl_tfilter; link_p[RTM_GETTFILTER-RTM_BASE].doit = tc_ctl_tfilter; link_p[RTM_GETTFILTER-RTM_BASE].dumpit = tc_dump_tfilter; }
通过以上注册的一系列函数,就可以完成tc filter的命令配置。
例如在控制台调用tc qdisc add dev eth0 handle ffff: ingress命令后,最终会调用到内核的tc_modify_qdisc函数,进入该函数后可以找到以下的代码片段: if (clid == TC_H_INGRESS)
q = qdisc_create(dev, tcm->tcm_parent, tca, &err); else
q = qdisc_create(dev, tcm->tcm_handle, tca, &err);
其中,qdisc_create函数用于创建struct Qdisc结构实例,在qdisc_create函数中进行了几个关键操作:
(1) 查找struct Qdisc_ops结构实例 struct Qdisc_ops *ops;
ops = qdisc_lookup_ops(kind); //用于查找与kind值相对应的struct Qdisc_ops的结构实例。
关于struct Qdisc_ops结构,在上文中已经大概描述过,他在模块初始化的时候通过调用
register_qdisc函数注册到链表中,并且每一个qidsc都会对应一个struct Qdisc_ops结构实例,但是并不是每一个qdisc都会对应一个struct Qdisc结构实例,只有那些已经配置在接口上的qdisc实例才会和struct Qdisc实例相对应。 (2) 分配struct Qdisc结构实例 struct Qdisc *sch;
sch = qdisc_alloc(dev, ops); //用于分配并初始化struct Qdisc结构实例
(3) 将新分配的struct Qdisc结构实例加入到设备qdisc链表中
list_add_tail(&sch->list, &dev->qdisc_list);
执行完qdisc_create函数后,会继续执行qdisc_graft函数,之后是dev_graft_qdisc函数,最后会让dev->qdisc_ingress指针指向刚才创建的struct Qdisc结构实例,这样基本上就完成了ingress qdisc的配置。
如前文所述,在打开了CONFIG_NET_CLS_ACT配置选项后,还必须通过tc filter命令配置polic扩展action,否则进入ingress处理流程后将什么都不会做。 2. Egress QOS
出口队列调度的入口点在src/net/core/dev.c的dev_queue_xmit函数中,代码片段如下: q = rcu_dereference(dev->qdisc); #ifdef CONFIG_NET_CLS_ACT
skb->tc_verd = SET_TC_AT(skb->tc_verd,AT_EGRESS); #endif
if (q->enqueue) {
/* Grab device queue */
spin_lock(&dev->queue_lock); q = dev->qdisc; if (q->enqueue) {
rc = q->enqueue(skb, q); qdisc_run(dev);
spin_unlock(&dev->queue_lock);
rc = rc == NET_XMIT_BYPASS ? NET_XMIT_SUCCESS : rc; goto out; }
spin_unlock(&dev->queue_lock); }
从上面的代码中可以看出,通过q = rcu_dereference(dev->qdisc)可以获取到设备上root qdisc的指针q(struct Qdisc *),在下面的处理过程中并没有判断q是否为NULL,这就说明设备上一定会存在egress qdisc,这一点和ingress是不同的,一个设备上可以没有ingress qdisc,即dev-> qdisc_ingress指针一般是NULL,除非通过tc qdisc命令配置了ingress qdisc。
我们看一下默认情况下dev->qdisc是如何配置的,在注册网络设备时会调用register_netdevice
函数,在register_netdevice函数中又会调用dev_init_scheduler函数,在该函数中的代码片段如下: dev->qdisc = &noop_qdisc;
dev->qdisc_sleeping = &noop_qdisc; INIT_LIST_HEAD(&dev->qdisc_list);
新注册一个接口后,dev->qdisc指针指向noop_qdisc结构实例,这是一个特殊的qdisc,它什么也不做。当创建好设备,用ifconfig up命令把设备拉起后,会调用到内核的src/net/core/dev.c中的dev_open函数,在dev_open函数中又会调用到src/net/sched/sch_generic.c中的dev_activate函数,代码片段如下:
if (dev->qdisc_sleeping == &noop_qdisc) { struct Qdisc *qdisc;
if (dev->tx_queue_len) {
qdisc = qdisc_create_dflt(dev, &pfifo_fast_ops, TC_H_ROOT);
if (qdisc == NULL) {
printk(KERN_INFO \ return; }
write_lock(&qdisc_tree_lock);
list_add_tail(&qdisc->list, &dev->qdisc_list); write_unlock(&qdisc_tree_lock); } else {
qdisc = &noqueue_qdisc; }
write_lock(&qdisc_tree_lock); dev->qdisc_sleeping = qdisc; write_unlock(&qdisc_tree_lock); } ……
rcu_assign_pointer(dev->qdisc, dev->qdisc_sleeping);
从上面的代码可以看出,当把设备拉起时给设备配置的默认root qdisc为pfifo_fast。之后我们可以在控制台调用tc qdisc add……命令配置其他的qdisc,配置过程与配置ingress qdisc的过程类似,在这里就不再赘述了。
下面我们以prio qdisc为例,看一下出口队列调度的大概流程。
假设我们已经通过tc qdisc命令在接口上配置了prio qdisc作为root qdisc,那么在dev_queue_xmit函数中调用了rc = q->enqueue(skb, q)后,就会调用到与prio qdisc对应的enqueue函数。对于prio qdisc来说,对应的enqueue函数是prio_enqueue(src/net/sched/sch_prio.c), 在prio_enqueue函数中代码片段如下:
qdisc = prio_classify(skb, sch, &ret); //进行分类选择,找出子qdisc
/*调用子qdisc的enqueue函数*/
if ((ret = qdisc->enqueue(skb, qdisc)) == NET_XMIT_SUCCESS) { sch->bstats.bytes += skb->len; sch->bstats.packets++;
sch->q.qlen++;
return NET_XMIT_SUCCESS; }
在上面的代码中首先通过调用prio_classify函数查找子qdisc,然后再调用子qdisc对应的enqueue函数。在这里我们要补充一些概念,在Linux QOS中qidsc分为两种,一种是有分类(classful)的qdisc,另一种是无分类(classless)的qdisc,有分类和无分类的qdisc都可以做为设备的root qdisc,但是有分类的qdisc通过它的分类(class)又可以嫁接出子qdisc,子qdisc可以是有分类的qdisc也可以是无分类的qdisc,最后的叶子qdisc则必须是无分类的qdisc,一般常用pfifo/bfifo做为叶子qdisc。因此可以利用这种组合的特性利用有分类qdisc和无分类qdisc组合出复杂的调度方式。下图简单的描述了他们之间的关系。
从上图中可以看出,当需要多种qdisc进行组合时,子qdisc必须嫁接在其父qdisc的某一个class上。
继续描述上面的prio qdisc,我们已经将prio qdisc配置成了root qdisc,在配置prio qdisc的过程中需要制定bands参数,这个参数指明了prio qdisc上的class数目,默认是3个class。配置了prio qdisc后会调用prio_init函数,该函数中会调用prio_tune函数,然后在prio_tune函数中会为prio qidsc创建默认的子qdisc,代码片段如下: for (i=0; i
if (q->queues[i] == &noop_qdisc) { struct Qdisc *child;
child = qdisc_create_dflt(sch->dev, &pfifo_qdisc_ops, TC_H_MAKE(sch->handle, i + 1)); if (child) {
sch_tree_lock(sch);
child = xchg(&q->queues[i], child);
if (child != &noop_qdisc) {
qdisc_tree_decrease_qlen(child, child->q.qlen);
qdisc_destroy(child); }
sch_tree_unlock(sch); } } }
从上面的代码可以看出,默认为prio qdisc创建的子qdisc为pfifo qdisc。
通过上面你的描述,我们了解了root qdisc和child qdisc之间的关系,下面我们继续描述prio_classify函数,代码片段如下所示:
struct prio_sched_data *q = qdisc_priv(sch); u32 band = skb->priority; struct tcf_result res;
*qerr = NET_XMIT_BYPASS;
if (TC_H_MAJ(skb->priority) != sch->handle) { #ifdef CONFIG_NET_CLS_ACT
switch (tc_classify(skb, q->filter_list, &res)) { case TC_ACT_STOLEN: case TC_ACT_QUEUED:
*qerr = NET_XMIT_SUCCESS; case TC_ACT_SHOT: return NULL; };
if (!q->filter_list ) { #else
if (!q->filter_list || tc_classify(skb, q->filter_list, &res)) { #endif
if (TC_H_MAJ(band)) band = 0;
return q->queues[q->prio2band[band&TC_PRIO_MAX]]; }
band = res.classid; }
band = TC_H_MIN(band) - 1; if (band > q->bands)
return q->queues[q->prio2band[0]];
return q->queues[band];
从上面的代码可以看出,首先会用skb->priority字段的高16位的值和sch->handle值进行比较,如果不相等的话就会调用tc_classify分类匹配函数进行处理。假如我们使用的是fw分类方法的话,就会调用到fw_classify函数,该函数的处理流程我们在前文中已经描述过了,在此不再赘述。
当找到子qdisc的指针后,就调用子qdisc的enqueue函数,如前文所述prio qidsc默认的子qdisc为pfifo qdisc,因此在这里会调用pfifo qdisc对应的enqueue函数pfifo_enqueue
(src/net/sched/sch_fifo.c),进入pfifo_enqueue函数,代码片段如下: struct fifo_sched_data *q = qdisc_priv(sch);
if (likely(skb_queue_len(&sch->q) < q->limit)) return qdisc_enqueue_tail(skb, sch);
return qdisc_reshape_fail(skb, sch);
用当前的队列长度和队列总长度进行比较(按数据包数量进行比较),如果没有超出队列长度限制则把数据包缓冲到队列的尾部,否则就调用qdisc_reshape_fail函数丢弃数据包。至此就完成了数据包的enqueue操作。
数据包入队操作完成后,接下来在dev_queue_xmit函数中会调用qdisc_run函数进行队列调度和出队列操作,在该函数中会调用__qdisc_run函数,在该函数中代码片段如下: while (qdisc_restart(dev) < 0 && !netif_queue_stopped(dev)) /* NOTHING */; out:
clear_bit(__LINK_STATE_QDISC_RUNNING, &dev->state);
可以看到在这里循环调用了qdisc_restart函数(src/net/sched/sch_generic.c),直到函数返回值不为负值或设备状态处于队列停止状态为止。进入qdisc_restart函数,代码片段如下: if (((skb = dev->gso_skb)) || ((skb = q->dequeue(q)))) { …… {
spin_unlock(&dev->queue_lock);
if (!netif_queue_stopped(dev)) { int ret;
ret = dev_hard_start_xmit(skb, dev); if (ret == NETDEV_TX_OK) { if (!nolock) {
netif_tx_unlock(dev); }
spin_lock(&dev->queue_lock); return -1; }
if (ret == NETDEV_TX_LOCKED && nolock) { spin_lock(&dev->queue_lock); goto collision; } } ……
spin_lock(&dev->queue_lock); q = dev->qdisc; }
requeue:
if (skb->next)
dev->gso_skb = skb; else
q->ops->requeue(skb, q); netif_schedule(dev); return 1; }
BUG_ON((int) q->q.qlen < 0); return q->q.qlen;
在这里调用了dequeue函数做出队列操作,对于prio qdisc来说会调用prio_dequeue函数,该函数的代码片段如下:
struct sk_buff *skb;
struct prio_sched_data *q = qdisc_priv(sch); int prio;
struct Qdisc *qdisc;
for (prio = 0; prio < q->bands; prio++) { qdisc = q->queues[prio]; skb = qdisc->dequeue(qdisc); if (skb) {
sch->q.qlen--; return skb; } }
return NULL;
这里对prio qdisc的所有bands(class)进行遍历,由于高优先级的class对应的的prio值也较小,因此直接遍历数组即可对优先级调度进行控制。最后还是会调用到子qidsc的dequeue函数获取数据包指针。当获取到的skb非空时直接返回,下次重新调用该函数时又会从高优先级的class开始遍历,这样就保证了每一次的dequeue操作总是从高优先级的class开始。
dequeue操作完成后,会调用dev_hard_start_xmit函数将数据包发送出去,如果发送成功则最终返回-1,在__qdisc_run函数中通过循环又会进入到qdisc_restart函数,直到队列为空为止。我们注意到在特殊情况下可能会有调用dev_hard_start_xmit发包不成功的情况,如果是因为发送设备忙碌造成的发送不成功则会进入requeue流程,将数据包重新缓冲到队列里,然后调用netif_schedule函数启用网络发包软中断处理流程,在软中断处理流程中会调用net_tx_action函数,最终又会调用qdisc_run进入队列调度流程。
3. 总结
以上对Ingress和Egress QOS的实现流程和框架进行了粗略分析,通过以上分析,希望读者可以从大的流程上了解QOS的实现框架。由于内核中实现了多种qdisc,在此不能一一赘述,有兴趣的读者可以参考本文的分析流程,切入到自己感兴趣的部分进行深入分析即可。但是万变不离其中,不论是哪种qdisc,都不会超出本文所描述的框架和流程,只不过在队列调度算法上会比较复杂罢了。
if (skb->next)
dev->gso_skb = skb; else
q->ops->requeue(skb, q); netif_schedule(dev); return 1; }
BUG_ON((int) q->q.qlen < 0); return q->q.qlen;
在这里调用了dequeue函数做出队列操作,对于prio qdisc来说会调用prio_dequeue函数,该函数的代码片段如下:
struct sk_buff *skb;
struct prio_sched_data *q = qdisc_priv(sch); int prio;
struct Qdisc *qdisc;
for (prio = 0; prio < q->bands; prio++) { qdisc = q->queues[prio]; skb = qdisc->dequeue(qdisc); if (skb) {
sch->q.qlen--; return skb; } }
return NULL;
这里对prio qdisc的所有bands(class)进行遍历,由于高优先级的class对应的的prio值也较小,因此直接遍历数组即可对优先级调度进行控制。最后还是会调用到子qidsc的dequeue函数获取数据包指针。当获取到的skb非空时直接返回,下次重新调用该函数时又会从高优先级的class开始遍历,这样就保证了每一次的dequeue操作总是从高优先级的class开始。
dequeue操作完成后,会调用dev_hard_start_xmit函数将数据包发送出去,如果发送成功则最终返回-1,在__qdisc_run函数中通过循环又会进入到qdisc_restart函数,直到队列为空为止。我们注意到在特殊情况下可能会有调用dev_hard_start_xmit发包不成功的情况,如果是因为发送设备忙碌造成的发送不成功则会进入requeue流程,将数据包重新缓冲到队列里,然后调用netif_schedule函数启用网络发包软中断处理流程,在软中断处理流程中会调用net_tx_action函数,最终又会调用qdisc_run进入队列调度流程。
3. 总结
以上对Ingress和Egress QOS的实现流程和框架进行了粗略分析,通过以上分析,希望读者可以从大的流程上了解QOS的实现框架。由于内核中实现了多种qdisc,在此不能一一赘述,有兴趣的读者可以参考本文的分析流程,切入到自己感兴趣的部分进行深入分析即可。但是万变不离其中,不论是哪种qdisc,都不会超出本文所描述的框架和流程,只不过在队列调度算法上会比较复杂罢了。
正在阅读:
Linux - QOS实现框架分析03-11
电气控制技术课后习题答案第一章07-03
05河北水工试验室试验检测方案05-15
大一高数(上)06-09
2016年教师培训微课设计模板07-23
电气控制与plc应用技术课后答案(全)06-05
《聊斋志异》中的海上异国形象04-20
分路局局长述职报告(精选多篇)09-26
社会实践简报小学生电子小报成品,简报报刊手抄报模板,画报剪报板报样板,电脑报纸示范样例A311-29
高二语文下册模块综合检测试题103-19
- exercise2
- 铅锌矿详查地质设计 - 图文
- 厨余垃圾、餐厨垃圾堆肥系统设计方案
- 陈明珠开题报告
- 化工原理精选例题
- 政府形象宣传册营销案例
- 小学一至三年级语文阅读专项练习题
- 2014.民诉 期末考试 复习题
- 巅峰智业 - 做好顶层设计对建设城市的重要意义
- (三起)冀教版三年级英语上册Unit4 Lesson24练习题及答案
- 2017年实心轮胎现状及发展趋势分析(目录)
- 基于GIS的农用地定级技术研究定稿
- 2017-2022年中国医疗保健市场调查与市场前景预测报告(目录) - 图文
- 作业
- OFDM技术仿真(MATLAB代码) - 图文
- Android工程师笔试题及答案
- 生命密码联合密码
- 空间地上权若干法律问题探究
- 江苏学业水平测试《机械基础》模拟试题
- 选课走班实施方案
- 框架
- 实现
- 分析
- Linux
- QOS
- “小学生学习对联,提高语文素养” 实验报告
- 陕西省素质教育督导评估316工程中等职业学校指标体系 - 图文
- 高中英语常见的可数不可数名词 分类总结
- 公司物品采购管理规定
- 2010年全国高考理科数学试题及答案-全国1
- 针灸科患者病情评估操作规范与流程
- 镇原县三岔中学新课程改革调研自查工作汇报
- 新建海南西环铁路XHZQ-2标环水保管理办法
- 财管整理
- 客运专线一跨式双层贝雷梁支架现浇箱梁施工技术研究报告 - 图文
- 成功赚钱的十七条金律
- 课程与教学论试题库
- 2008年度云南省增值电信业务经营许可证年检公告合格企业 - 图文
- 核技术利用单位辐射安全管理标准化建设项目表
- 电话语音客服系统说明书
- 一年级上学期科研总结报告
- 张泾中学综合楼施工组织设计 - 图文
- 高一英语必修一1-5单元知识点
- 支气管炎的治疗调理方法
- 爱心献血倡议书