
QOS的控制分为Ingress 和Egress。这里主要分析出口.
调试需要iproute2的tc :
点击(此处)折叠或打开
Linux Traffic Control is configured with the utility tc. It is part of the iproute2 package. Some Linux distributions already include tc, but it may be an old version, without support for Diffserv.
点击(此处)折叠或打开
调试版本iproute2-4.2.0
那么编译iproute2需要依赖的东西:
Bison
Flex
Libdb-dev
找到tc的源码后,我们先看看tc命令的主程序 tc.c
点击(此处)折叠或打开
int main(int argc, char **argv)
{
int ret
char *batch_file = NULL
while (argc >1) {
if (argv[1][0] != '-')
break
if (matches(argv[1], "-stats") == 0 ||
matches(argv[1], "-statistics") == 0) {
++show_stats
} else if (matches(argv[1], "-details") == 0) {
++show_details
} else if (matches(argv[1], "-raw") == 0) {
++show_raw
} else if (matches(argv[1], "-pretty") == 0) {
++show_pretty
} else if (matches(argv[1], "-graph") == 0) {
show_graph = 1
} else if (matches(argv[1], "-Version") == 0) {
printf("tc utility, iproute2-ss%s\n", SNAPSHOT)
return 0
} else if (matches(argv[1], "-iec") == 0) {
++use_iec
} else if (matches(argv[1], "-help") == 0) {
usage()
return 0
} else if (matches(argv[1], "-force") == 0) {
++force
} else if (matches(argv[1], "-batch") == 0) {
argc-- argv++
if (argc <= 1)
usage()
batch_file = argv[1]
} else if (matches(argv[1], "-netns") == 0) {
NEXT_ARG()
if (netns_switch(argv[1]))
return -1
} else if (matches(argv[1], "-names") == 0 ||
matches(argv[1], "-nm") == 0) {
use_names = true
} else if (matches(argv[1], "-cf") == 0 ||
matches(argv[1], "-conf") == 0) {
NEXT_ARG()
conf_file = argv[1]
} else {
fprintf(stderr, "Option \"%s\" is unknown, try \"tc -help\".\n", argv[1])
return -1
}
argc-- argv++
}
if (batch_file)
return batch(batch_file)
if (argc <= 1) {
usage()
return 0
}
tc_core_init()
if (rtnl_open(&rth, 0) <0) {
fprintf(stderr, "Cannot open rtnetlink\n")
exit(1)
}
if (use_names &&cls_names_init(conf_file)) {
ret = -1
goto Exit
}
ret = do_cmd(argc-1, argv+1)
Exit:
rtnl_close(&rth)
if (use_names)
cls_names_uninit()
return ret
}
获取内核路由表以及 *** 作内核路由表有几种方法:读proc 或者用ioctl(sock_fd, SIOCADDRT, &rt),这里的第二个参数是设置路由表,读也有相应的参数,还有第三种方法就是用netlink接口对内核路由表进行读取、增加、删除 *** 作如linaxing(牛牛)所说,以前是用IOCTL,不过那个读出的和netlink的有点差别,是信息量有差别.具体我也说不清楚,可查看相关maillist,那个牛人也就说了一句话
下面给出偶自己读内核路由表的一个程序,仿照zebra的用法
不过,最后读出的内容有点问题,好像还得转换一下,实在写不动了,欢迎批评!
#include <stdio.h>
#include <string.h>
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
//#include <sys/types.h>
//#include <linux/uio.h>
#include <errno.h>
#ifdef SEQ
struct rtnl_handle
{
unsigned int seq
}
#endif
static void parse_rtattr(struct rtattr **tb, int max,
struct rtattr *rta, int len)
{
while(RTA_OK(rta, len))
{
if(rta->rta_type <= max)
tb[rta->rta_type] = rta
rta = RTA_NEXT(rta, len)
}
}
int routeprint( struct sockaddr_nl *snl, struct nlmsghdr *h2)
{
#if 1
struct rtmsg *rtm
struct rtattr *tb[RTA_MAX + 1]
int len
int index
int table
void* dest
void* gate
char dest2[100]
rtm = NLMSG_DATA(h2)//get the data portion of "h2 "
index = 0
dest = NULL
gate = NULL
table = rtm->rtm_table
len = h2->nlmsg_len - NLMSG_LENGTH(sizeof(struct rtmsg))
memset(tb, 0, sizeof tb)
parse_rtattr(tb, RTA_MAX, RTM_RTA(rtm), len)
if(tb[RTA_OIF])
index = *(int *)RTA_DATA(tb[RTA_OIF])
if(tb[RTA_DST]){
dest = RTA_DATA(tb[RTA_DST])
// printf( "debug dest\n ")
}
else dest = 0
#if 1
if(tb[RTA_METRICS]){
gate = RTA_DATA(tb[RTA_METRICS])
}
#else
if(tb[RTA_GATEWAY]){
gate = RTA_DATA(tb[RTA_GATEWAY])
//iprintf( "debug gate\n ")
}
#endif
printf( "family:%d\t ",rtm->rtm_family)
printf( "index: %d\t ", index)
// memcpy(dest2, dest, 4)
printf( "dest: %d\t ", dest)
// printf( "dest: %c\t ", dest2[1])
// printf( "dest: %c\t ", dest2[2])
// printf( "dest: %c\t ", dest2[3])
printf( "gate: %d\n ", gate)
#endif
return 1
}
#ifdef SEQ
int getroute(int sockfd,struct rtnl_handle *rtnl)
#else
int getroute(int sockfd)
#endif
{
int i
int status, sendsize
unsigned char buf[8192]
struct iovec iov = {(void*)buf, sizeof(buf)}
struct sockaddr_nl nladdr
struct nlmsghdr *h
struct
{
struct nlmsghdr nlh
struct rtgenmsg g
}req
struct msghdr msg = { (void*)&nladdr, sizeof(nladdr),
&iov, 1, NULL, 0, 0}
nladdr.nl_family = AF_NETLINK
req.nlh.nlmsg_len = sizeof(req)
req.nlh.nlmsg_type = RTM_GETROUTE//增加或删除内核路由表相应改成RTM_ADDROUTE和RTM_DELROUTE
req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST
req.nlh.nlmsg_pid = 0
#ifdef SEQ
req.nlh.nlmsg_seq = ++rtnl->seq//may be 0?
#else
//int i
//if (i > 4096) i = 1
req.nlh.nlmsg_seq = 1
#endif
req.g.rtgen_family = AF_INET
printf( "sockfd: %d\n ", sockfd)
if((sendsize=sendto(sockfd, (void*)&req, sizeof(req), 0,
(struct sockaddr*)&nladdr, sizeof(nladdr))) < 0){
perror( "sendto ")
return -1
}
printf( "sendsize= %d\n ",sendsize)
if((status=recvmsg(sockfd, &msg, 0)) < 0){
perror( "recvmsg ")
return -1
}
printf( "status= %d\n ",status)
#if 1 //segmentation fault
for(h = (struct nlmsghdr*)buf NLMSG_OK(h, status)
h = NLMSG_NEXT(h, status))
{
if(h->nlmsg_type == NLMSG_DONE)
{
printf( "finish reading\n ")
return 1
}
if(h->nlmsg_type == NLMSG_ERROR)
{
printf( "h:nlmsg ERROR ")
return 1
}
routeprint(&nladdr, h)
}
#endif
// printf( "Can 't convert 'h '\n ")
// routeprint(h)
return 1
}
int main()
{
int sockfd
#ifdef SEQ
struct rtnl_handle rth
#endif
struct sockaddr_nl nladdr
if((sockfd = socket(AF_NETLINK, SOCK_RAW,
NETLINK_ROUTE)) <0){
perror( "netlink socket ")
return -1
}
nladdr.nl_family = AF_NETLINK
nladdr.nl_pad = 0
nladdr.nl_pid = 0
nladdr.nl_groups = RTMGRP_LINK|RTMGRP_IPV4_ROUTE|
RTMGRP_IPV4_IFADDR
if(bind(sockfd, (struct sockaddr*)&nladdr,
sizeof(nladdr)) < 0){
perror( "bind ")
close(sockfd)
return -1
}
#ifdef SEQ
if(getroute(sockfd, &rth) < 0){
#else
if(getroute(sockfd) < 0){
#endif
perror( "can 't get route\n ")
return -1
}
return 1
}
上一篇文章着重的聊了socket服务端的bind,listen,accpet的逻辑。本文来着重聊聊connect都做了什么?
如果遇到什么问题,可以来本文 https://www.jianshu.com/p/da6089fdcfe1 下讨论
当服务端一切都准备好了。客户端就会尝试的通过 connect 系统调用,尝试的和服务端建立远程连接。
首先校验当前socket中是否有正确的目标地址。然后获取IP地址和端口调用 connectToAddress 。
在这个方法中,能看到有一个 NetHooks 跟踪socket的调用,也能看到 BlockGuard 跟踪了socket的connect调用。因此可以hook这两个地方跟踪socket,不过很少用就是了。
核心方法是 socketConnect 方法,这个方法就是调用 IoBridge.connect 方法。同理也会调用到jni中。
能看到也是调用了 connect 系统调用。
文件:/ net / ipv4 / af_inet.c
在这个方法中做的事情如下:
注意 sk_prot 所指向的方法是, tcp_prot 中 connect 所指向的方法,也就是指 tcp_v4_connect .
文件:/ net / ipv4 / tcp_ipv4.c
本质上核心任务有三件:
想要能够理解下文内容,先要明白什么是路由表。
路由表分为两大类:
每个路由器都有一个路由表(RIB)和转发表 (fib表),路由表用于决策路由,转发表决策转发分组。下文会接触到这两种表。
这两个表有什么区别呢?
网上虽然给了如下的定义:
但实际上在Linux 3.8.1中并没有明确的区分。整个路由相关的逻辑都是使用了fib转发表承担的。
先来看看几个和FIB转发表相关的核心结构体:
熟悉Linux命令朋友一定就能认出这里面大部分的字段都可以通过route命令查找到。
命令执行结果如下:
在这route命令结果的字段实际上都对应上了结构体中的字段含义:
知道路由表的的内容后。再来FIB转发表的内容。实际上从下面的源码其实可以得知,路由表的获取,实际上是先从fib转发表的路由字典树获取到后在同感加工获得路由表对象。
转发表的内容就更加简单
还记得在之前总结的ip地址的结构吗?
需要进行一次tcp的通信,意味着需要把ip报文准备好。因此需要决定源ip地址和目标IP地址。目标ip地址在之前通过netd查询到了,此时需要得到本地发送的源ip地址。
然而在实际情况下,往往是面对如下这么情况:公网一个对外的ip地址,而内网会被映射成多个不同内网的ip地址。而这个过程就是通过DDNS动态的在内存中进行更新。
因此 ip_route_connect 实际上就是选择一个缓存好的,通过DDNS设置好的内网ip地址并找到作为结果返回,将会在之后发送包的时候填入这些存在结果信息。而查询内网ip地址的过程,可以成为RTNetLink。
在Linux中有一个常用的命令 ifconfig 也可以实现类似增加一个内网ip地址的功能:
比如说为网卡eth0增加一个IPV6的地址。而这个过程实际上就是调用了devinet内核模块设定好的添加新ip地址方式,并在回调中把该ip地址刷新到内存中。
注意 devinet 和 RTNetLink 严格来说不是一个存在同一个模块。虽然都是使用 rtnl_register 注册方法到rtnl模块中:
文件:/ net / ipv4 / devinet.c
文件:/ net / ipv4 / route.c
实际上整个route模块,是跟着ipv4 内核模块一起初始化好的。能看到其中就根据不同的rtnl *** 作符号注册了对应不同的方法。
整个DDNS的工作流程大体如下:
当然,在tcp三次握手执行之前,需要得到当前的源地址,那么就需要通过rtnl进行查询内存中分配的ip。
文件:/ include / net / route.h
这个方法核心就是 __ip_route_output_key .当目的地址或者源地址有其一为空,则会调用 __ip_route_output_key 填充ip地址。目的地址为空说明可能是在回环链路中通信,如果源地址为空,那个说明可能往目的地址通信需要填充本地被DDNS分配好的内网地址。
在这个方法中核心还是调用了 flowi4_init_output 进行flowi4结构体的初始化。
文件:/ include / net / flow.h
能看到这个过程把数据中的源地址,目的地址,源地址端口和目的地址端口,协议类型等数据给记录下来,之后内网ip地址的查询与更新就会频繁的和这个结构体进行交互。
能看到实际上 flowi4 是一个用于承载数据的临时结构体,包含了本次路由 *** 作需要的数据。
执行的事务如下:
想要弄清楚ip路由表的核心逻辑,必须明白路由表的几个核心的数据结构。当然网上搜索到的和本文很可能大为不同。本文是基于LInux 内核3.1.8.之后的设计几乎都沿用这一套。
而内核将路由表进行大规模的重新设计,很大一部分的原因是网络环境日益庞大且复杂。需要全新的方式进行优化管理系统中的路由表。
下面是fib_table 路由表所涉及的数据结构:
依次从最外层的结构体介绍:
能看到路由表的存储实际上通过字典树的数据结构压缩实现的。但是和常见的字典树有点区别,这种特殊的字典树称为LC-trie 快速路由查找算法。
这一篇文章对于快速路由查找算法的理解写的很不错: https://blog.csdn.net/dog250/article/details/6596046
首先理解字典树:字典树简单的来说,就是把一串数据化为二进制格式,根据左0,右1的方式构成的。
如图下所示:
这个过程用图来展示,就是沿着字典树路径不断向下读,比如依次读取abd节点就能得到00这个数字。依次读取abeh就能得到010这个数字。
说到底这种方式只是存储数据的一种方式。而使用数的好处就能很轻易的找到公共前缀,在字典树中找到公共最大子树,也就找到了公共前缀。
而LC-trie 则是在这之上做了压缩优化处理,想要理解这个算法,必须要明白在 tnode 中存在两个十分核心的数据:
这负责什么事情呢?下面就简单说说整个lc-trie的算法就能明白了。
当然先来看看方法 __ip_dev_find 是如何查找
文件:/ net / ipv4 / fib_trie.c
整个方法就是通过 tkey_extract_bits 生成tnode中对应的叶子节点所在index,从而通过 tnode_get_child_rcu 拿到tnode节点中index所对应的数组中获取叶下一级别的tnode或者叶子结点。
其中查找index最为核心方法如上,这个过程,先通过key左移动pos个位,再向右边移动(32 - bits)算法找到对应index。
在这里能对路由压缩算法有一定的理解即可,本文重点不在这里。当从路由树中找到了结果就返回 fib_result 结构体。
查询的结果最为核心的就是 fib_table 路由表,存储了真正的路由转发信息
文件:/ net / ipv4 / route.c
这个方法做的事情很简单,本质上就是想要找到这个路由的下一跳是哪里?
在这里面有一个核心的结构体名为 fib_nh_exception 。这个是指fib表中去往目的地址情况下最理想的下一跳的地址。
而这个结构体在上一个方法通过 find_exception 获得.遍历从 fib_result 获取到 fib_nh 结构体中的 nh_exceptions 链表。从这链表中找到一模一样的目的地址并返回得到的。
文件:/ net / ipv4 / tcp_output.c
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)