基于Linux的QoS编程接口研究与分析(7)

4.1 QoS 数据通道
全局的数据通道的略图见图4.1,灰色部分是 QoS:

图 4.1 网络数据加工
数据包的处理过程是:Input Interface → Ingress Policing → Input Demultiplexing (判断分组是本地使用还是转发) → Forwarding → Output Queuing → Output Interface。入口的流量限制(Ingress Policing)和出口 的队列调度(Output Queuing)是由 Linux 核心的流量控制的代码实现的。入口 的流量限制(Ingress Policing)丢弃不符合规定的分组,确保进入的各个业务 流分组速率的上限,出口的队列调度(Output Queuing)依据配置实现分组的排 队、丢弃。
以下是分析Linux 2.4 的代码得出的数据通道,根据在图4.1中的位置,分为 Ingress policing的数据通道和 Output Queuing 的数据通路,一下分别阐述:
4.1.1 Ingress policing 的数据通道
Ingress policing 的 QoS 的部分是通过 HOOK 实现的。HOOK 是 Linux 实现 netfilter 的重要手段,数据从接收到转发得通路上有5个HOOK点①,每个 HOOK点有若干个挂钩处理函数: 
(typedef unsigned int nf_hookfn(unsigned int hooknum, 
struct sk_buff **skb,
const struct net_device *in, 
const struct net_device *out, 
int (*okfn)(struct sk_buff *)); )。Ingress policing 用 的 是 NF_IP_PRE_ROUTING 这个 HOOK 点,其挂钩处理函数用的是 net/sched/sch_ingress.c ing_hook()。Ingress policing 的数据通道的略图,灰色部分是 QoS,黑色部分是 HOOK 的处理过程②:

图 4.2 Ingress policing 的总概图
1其中,HOOK 点NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测),源地址转换在此点进行;NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT 包过滤在此点进行; NF_IP_FORWARD:要转发的包通过此检测点,FORWORD 包过滤在此点进行;NF_IP_LOCAL_OUT:本机进程发出 的包通过此检测点,OUTPUT 包过滤在此点进行;NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的 包通过此检测点,内置的目的地址转换功能(包括地址伪装)在此点进行
2以下数据通路是以 ipv4 为例,ipv6 类似

图 4.3 Ingress policing 的数据通道的略图
netif_rx()-> 数据接收队列队列-> Bottom Half③ 程序中通过软中断调用 net_rx_action()将硬件层得到的数据传输到IP层->
ip_rcv() #net/ipv4/ip_input.c 丢弃校验和不正确的ip包->
nf_hook_slow()#net/core/netfilter.c->
nf_iterate()#net/core/netfilter.c->
ing_hook() ④#net/sched/sch_ingress.c->
QoS:如果设置了 qdisc_ingress,则调用 ingress_dequeue()⑤,此处可以对流量进行限制#net/sched/sch_ingress.c-> 
ip_rcv_finish()#net/ipv4/ip_input.c(sch_ingress.c 的 enqueue()有限流制作用,然而 dequeue() 却是空函数。)->
以下路由:ip_route_input() #net/ipv4/route.c->
如 果 转 发 ip_route_input_slow() #net/ipv4/route.c , 如 果 本 地 处 理 ip_route_input_mc() #net/ipv4/route.c
3 为了避免处理长中断服务程序的时候,关中时间过长,Linux 将中断服务程序一分为二,各称作 top half和 bottom half,前者读取来自设备的数据,保存到预定的缓冲区(队列),然后通知 bottom half 在适当的时候完成。

4 Ingress 模块初始化时,把 ing_hook()注册为挂钩(HOOK)处理函数 
5 Qdisc_ingress 不是一个队列,它只有 ingress_enqueue(),而 ingress_dequeue()只是一个空函数。把ingress_enqueue()理解为一个限流阀门更准确。

4.1.2 Output Queuing 的数据通路
Output Queuing 的数据通路略图, 灰色部分是 QoS:

图4.4 Output Queuing 的数据通路略图
这个部分描述转发数据的数据通路,位于路由之后:
ip_forward() #net/ipv4/ip_forward.c->
ip_forward_finish() #net/ipv4/ip_forward.c->
ip_send()#include/net/ip.h->ip_finish_output()#net/ipv4/ip_output.c->
ip_finish_output2() #net/ipv4/ip_output.c->
neigh_resolve_output()orneigh_connected_output()#net/core/neighbour.c->
dev_queue_xmit() #net/core/dev.c->
QoS:如果有排队方式,那么skb⑥先进入排队 q->enqueue(skb,q),然后运行qdisc_run() 
#include/net/pkt_sched.h: 
while (!netif_queue_stopped(dev) && qdisc_restart(dev)<0);
->
qdisc_restart(dev)#net/sched/sch_generic.c->#linux/net/pkt_sched.h
而后, q->dequeue(q)
………………

4.2 Sched 的代码结构

在 net/sched/目录下放着 Linux 目前已经实现的用于路由转发调度的各个算法,例如 cbq、tbf 等等。 这个目录下的文件大体上可以分为三个部分: 
1) sch*.c:sch_api.c、sch_generic.c 和 sch_atm.c、sch_cbq.c、 sch_csz.c、sch_dsmark.c、sch_fifo.c、 sch_gred.c、sch_htb.c、sch_ingress.c、sch_prio.c、sch_red.c、sch_sfq.c、sch_tbf.c、sch_teql.c, 前 2 个提供一些通用的函数,后面几个是模块化的程序,以注册和注销一个结构体变量 作为模块的初始化和清除。
6 Linux 中,在 INET Socket 层及其以下层次之间数据包的传递是通过 struct sk_buff{}结构完成的。
例如对于 sch_tbf.c,这个文件实现的是令牌桶算法,最后生成一个 struct Qdisc_ops 的结 构 变 量 tbf_qdisc_ops , 在 模 块 初 始 化 的 时 候 , 注 册 tbf_qdisc_ops , register_qdisc(&tbf_qdisc_ops),注册的过程其实就是加入一个链表的过程,sch_api.c 提 供这个注册的函数。以下是 sch_tbf.c 文件的一部分代码,其余的 sch*.c 的第二部分的文 件与之类似:
struct Qdisc_ops tbf_qdisc_ops = 
{ NULL, NULL, "tbf", 
sizeof(struct tbf_sched_data), 
tbf_enqueue, tbf_dequeue, 
tbf_requeue,
tbf_drop, 
tbf_init, 
tbf_reset,
tbf_destroy,
tbf_change, 
tbf_dump, };

#ifdef MODULE 
int init_module(void) {
return register_qdisc(&tbf_qdisc_ops); 
}

void cleanup_module(void) {
unregister_qdisc(&tbf_qdisc_ops);
} #endif 
sch*.c 的第二部分的文件定义了、、、,有 enqueue……….
2) cls*.*:cls_api.c 和 cls_fw.c、cls_route.c、cls_rsvp.c、cls_rsvp6.c、cls_tcindex.c、cls_u32.c, cls_rsvp.h。前者提供分类通用的函数,后面几个是模块化的东西,以注册和注销一个结 构体变量作为模块的初始化和清除。 
3) estimator.c 和 police.c,提供一些通用的函数。
关键数据结构 struct Qdisc_ops { 
struct Qdisc_ops *next;
struct Qdisc_class_ops *cl_ops;
char id[IFNAMSIZ];
int priv_size; 

int (*enqueue)(struct sk_buff *, struct Qdisc *);
struct sk_buff* (*dequeue)(struct Qdisc *);
int (*requeue)(struct sk_buff *, struct Qdisc *);
int (*drop)(struct Qdisc *); 

int (*init)(struct Qdisc *, struct rtattr *arg); 
void (*reset)(struct Qdisc *); 
void (*destroy)(struct Qdisc *); 
int (*change)(struct Qdisc *, struct rtattr *arg);

int (*dump)(struct Qdisc *, struct sk_buff *);
};

4.3 iprouter2的代码结构

iproute2 是一个用户空间的程序,它的功能是解释以 tc8开头的命令,如果解释成功,把 它们通过 AF_NETLINK 的 socket 传给 Linux 的内核空间。 iproute2 的代码主要有 include、ip、lib、misc、tc 目录组成,misc 的代码量很少,并且 作用不大,此处略去。Include 是一个包含头文件的目录,这个目录下的头文件会被其他目 录下的*.c 文件所用,lib 目录定义了一些通用的函数,例如与向 linux 系统传递 tc 参数的方 法:例如 rtnl_talk9, rtnl_send10等等,此点在第 4 节中有详细的介绍。Ip 目录代码主要用于解 释路由的命令,使得流量的控制策略可以与路由挂钩。不过这不是本文想要详细讨论的。tc 目录的代码是 Tc 的最为主要的部分,解释了流量控制和整形的大部分命令。 tc 目录的代码分为四个部分,f_*.c、q_*.c、m_*.c、tc_*.c+tc*.h:
1) f_*.c,解释各种分类器(filter),与 sched/cls_*.c 相对应。
2)q_*.c,解释各种队列规范(qdisc)…..与 sched/sch_*.c 相对应。
3)m_*.c,这部分就两个文件:m_estimator.c 和 m_police.c,分别对应于 sched/estimator.c 和 sched/police.c 。
4)tc_*.c+tc.h,主控文件 tc.c,把解释任务分给 tc_qdisc.c、tc_filter.c、tc_class.c 中的函数。
以下是 tc.c/main()中的代码:
if (argc > 1) { 
if (matches(argv[1], "qdisc") == 0) return do_qdisc(argc-2, argv+2); 
if (matches(argv[1], "class") == 0) return do_class(argc-2, argv+2); 
if (matches(argv[1], "filter") == 0) return do_filter(argc-2, argv+2); 
if (matches(argv[1], "help") == 0) usage();
fprintf(stderr, "Object "%s" is unknown, try "tc help".n", argv[1]); 
exit(-1); 
}
iproute2 提供给命令的详解可见[HMM+02],附一提供常用的命令。

4.4 Sched 与 iproute2 的通信:AF_NETLINK

Sched 与 iproute2 的通信,是典型的 Linux 内核模块和用户空间的进程之间的通信,这 种通信一般由 Netlink Socket 来提供这种双向的通信连接。这种连接由标准的提供给用户进 程的 socket 和提供给内核模块的 API 组成,用户空间的接口简单的说就是创建一个 family 为 AF_NETLINK 的 socket,然后使用这个 socket 进行通信,自然,用户空间的进程除了 sock_fd = socket(AF_NETLINK ,SOCK_RAW, ……); 是看不到这与其他的通信方式(例如 AF_INET)有任何的不同; 内核空间维护一张 link_rtnetlink_table 表 rtnetlinks[][]。 
以下结合 iproute2 控制 Linux 的 TC 模块,通过一个例子分析控制通路,得到用户空间 发送、用户空间接收、内核发送、内核接收的一些视图。
一个命令:tc qdisc add dev eth1…… 
这个命令为某个网络接口 eth1 增加一个 qdisc。 
命令首先在用户空间被 iproute2 分析: 
1)分析 tc:main(int argc, char **argv)被调用,此函数在 tc/tc.c 中; 
2)分析 tc qdisc:do_qdisc(argc-2, argv+2);被调用,此函数在 tc/tc_qdisc.c 中; 
3)分析 tc qdisc add: tc_qdisc_modify(RTM_NEWQDISC, NLM_F_EXCL|NLM_F_CREATE, argc-1, argv+1); 被调用,此函数在 tc/tc_qdisc.c 中,在这个函数中,将分析完这一行 tc 的命令,各种参数(例如 RTM_NEWQDISC) 被写到 netlink 包中,并且开始与核心通信。
在用户空间中,这个顺序为 rtnl_open rtnl_talk rtnl_close,rtnl_open 的作用是打开一个 AF_NETLINK 的 socket,rtnl_close 的作用是关闭打开的 AF_NETLINK 的 socket,
int rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n,
pid_t peer, unsigned groups,
struct nlmsghdr *answer, 
int (*junk)(struct sockaddr_nl *,struct nlmsghdr *n, 
void *), void *jarg),
这个函数是 iproute2 与 linux 内核通信的一个库函数,是属于用户空间的函数。 
用户空间通信前的准备:填充 netlink 包;然后把 netlink 包发送到内核空间去。详见以下代 码。
if (k[0]) 
addattr_l(&req.n, sizeof(req), TCA_KIND, k, strlen(k)+1);
if (est.ewma_log) 
addattr_l(&req.n, sizeof(req), TCA_RATE, &est, sizeof(est)); 
/* 通过这个函数,所有的参数都被填充进 netlink 的包中 */ 
……… 
if (rtnl_open(&rth, 0) < 0) {
fprintf(stderr, "Cannot open rtnetlinkn");
exit(1); 

if (d[0]) { 
int idx;

ll_init_map(&rth);

if ((idx = ll_name_to_index(d)) == 0) {
fprintf(stderr, "Cannot find device "%s"n", d); 
exit(1);

req.t.tcm_ifindex = idx;
}

if (rtnl_talk(&rth, &req.n, 0, 0, NULL, NULL, NULL) < 0) 
/*在此之前,已经通过 rtnl_open(&rth, 0))打开一个 socket*/ 
exit(2); 
rtnl_close(&rth);
(rtnl_talk(&rth, &req.n, 0, 0, NULL, NULL, NULL) < 0)的发送过程包括 sendmsg 和 recvmsg,具体为:
int rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n, pid_t peer,
unsigned groups, struct nlmsghdr *answer,
int (*junk)(struct sockaddr_nl *,struct nlmsghdr *n, void *),
void *jarg)

{
…………
struct sockaddr_nl nladdr;
struct iovec iov = { (void*)n, n->nlmsg_len };
char buf[8192]; 
struct msghdr msg = { 
(void*)&nladdr, sizeof(nladdr),
&iov, 1,
NULL,0,
0,
}
……
status = sendmsg(rtnl->fd, &msg, 0);
……
iov.iov_base = buf; 
while (1) { 
……
status = recvmsg(rtnl->fd, &msg, 0);
…… 
}
}
内核模块的初始化:在 net/sched/sch_api.c 文件中的 void __init pktsched_init (void)函数 中,初始化了 link_rtnetlink_table 表,link_rtnetlink_table 是一张 struct rtnetlink_link 的表 
struct rtnetlink_link { 
int (*doit)(struct sk_buff *, struct nlmsghdr*, void *attr);
int (*dumpit)(struct sk_buff *, struct netlink_callback *cb);
};
struct rtnetlink_link 由函数指针 doit 和 dumpit 组成,这张表可以由需要执行的动作的宏定义 (例如:RTM_NEWQDISC,RTM_DELQDISC)来索引,以使得能通过这张表调动相应的 函数。内核模块从用户空间收到的就是这些索引和参数,以此调用注册在此表中的函数。 

link_p = rtnetlink_links[PF_UNSPEC]; 
/* Setup rtnetlink links. It is made here to avoid exporting large number of public symbols. */ 
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; 
}
下面具体分析一下这个通信的过程: 
用户空间: 
用户空间发送 
rtnl_talk() #iproute2/lib/libnetlink.c->
sendmsg(rtnl->fd,&msg,0) #net/socket.c->
sock_sendmsg(sock, &msg_sys, total_len) #net/socket.c->
sock->ops->sendmsg(sock, msg, size, &scm) #net/socket.c,在这里,通过函数指针, 调用了 static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, int len,struct scm_cookie *scm) 这在 af_netlink.c 中定义⑦。 
netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);或者 netlink_broadcast(sk, skb, dst_pid, dst_groups, GFP_KERNEL);

用户空间接收 rtnl_talk() #iproute2/lib/libnetlink.c->
while{… 
status = recvmsg(…); #net/socket.c …. 
}->
sock_recvmsg(sock, &msg_sys, total_len, flags); #net/socket.c->
sock->ops-> recvmsg(sock, msg, size, flags, &scm); #net/socket.c 在这里,通过函数指 针调用了 static int netlink_recvmsg(struct socket *sock, struct msghdr *msg, int len, int flags, struct scm_cookie *scm),这在 af_netlink.c 中定义。
⑦netnetlinkaf_netlink.c 中定义
struct proto_ops netlink_ops = {
family: PF_NETLINK, 
release: netlink_release, 
bind: netlink_bind, 
connect: netlink_connect,
socketpair: sock_no_socketpair, 
accept: sock_no_accept, 
getname: netlink_getname,
poll: datagram_poll, 
ioctl:sock_no_ioctl, 
listen: sock_no_listen,
shutdown: sock_no_shutdown, 
setsockopt: sock_no_setsockopt,
getsockopt:sock_no_getsockopt, 
sendmsg: netlink_sendmsg, 
recvmsg: netlink_recvmsg, 
mmap: sock_no_mmap, 
sendpage: sock_no_sendpage,
};
在内核中: 
内核发送 例如 qdisc_notify(skb, n, clid, NULL, q);
#net/sched/sch_api.c->
rtnetlink_send(skb, pid, RTMGRP_TC, n->nlmsg_flags&NLM_F_ECHO); #net/core/rtnetlink.c->
netlink_broadcast(rtnl, skb, pid, group, GFP_KERNEL); 和
netlink_unicast(rtnl, skb, pid, MSG_DONTWAIT); 这在 af_netlink.c 中定义。 
内核接收(关于这个部分,我想还需要修改) 
void __init rtnetlink_init() #net/core/rtnetlink.c->
netlink_kernel_create(NETLINK_ROUTE, rtnetlink_rcv) 
#net/netlink/af_netlink.c:
{ …… 
if (input) 
sk->protinfo.af_netlink->data_ready = input;
……
} 在 af_netlink.c 中定义,通过函数指针调用->
static void rtnetlink_rcv(struct sock *sk, int len) #net/core/rtnetlink.c
{ ……
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
if (rtnetlink_rcv_skb(skb)) {
if (skb->len) 
skb_queue_head(&sk->receive_queue, skb); 
else 
kfree_skb(skb); 
break; 

kfree_skb(skb); 

…… 

->
renetlink_rcv_skb() #net/core/rtnetlink.c->
rtnetlink_rcv_msg() #net/core/rtnetlink.c : 
{
… 
link = &(rtnetlink_links[PF_UNSPEC][type]); 

Err = link->doit(skb,nlh,(void*)rta); /*此处核心调用了 link_rtnetlink_table 表中的相 应于用户命令的方法被调用*/
… 

4.5 小结

第五章 QoS的未来

在下一代网络标准体系中,QoS(服务质量保证)是极为关键的内容。目前ITU-TSG13在这一方面主要围绕着三个草案建议开展工作:Y.qosar、Y.123.qos、Y.e2eqos。在最近一次会议上通过了Y.qosar。
Y.qosqr草案建议提供了在分组网络中支持QoS的框架。这一框架结构由一组QoS构建模块组成。这些模块跨越了三个逻辑层面,即控制平面、数据平面和管理平面。它们组合起来可以达到控制网络性能的目的。最终借助这些模块的组合应用可以帮助电信营运商提供综合的业务性能,从而确定出针对某种业务的客户满意度。
Y.123.qos草案建议给出了基于以太的IP接入网络的QoS架构。它规定了一个分层的参考模型,并给出了协议需求及实施方法。在Y.123.qos中针对此架构所涉及的拓扑及资源状态收集需求及信令描述也进行了规定。
Y.e2e草案建议给出了从IP网络演进成为NGN网络的一种端到端QoS架构。尤其是在核心网络中,MPLS作为一种关键技术应得以支持,从而方便地提供VPN、流量工程和QoS路由。国际电联下一步将在以下方面开展工作:进一步增强计费灵活性和商业应用模式的研究;进一步深入研究业务控制层与承载层间的接口与控制信息的交互;加强域间QoS控制的研究;进一步包含无连接业务的情况,增大协议适用范围;增加对移动性的支持。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值