DR模式的原理通过上篇笔记已经了解的比较透彻了,只不过是用的LVS来模拟进行抓包分析的,但是DR模式的原理是一样的,所以先跟着DPVS的源码了解一下DPVS实现DR模式的流程(这次分析主要跟着DR模式的处理流程走,会跳过其他的处理流程)。先跳过DPVS的启动流程,从main函数的netif_lcore_start()函数开始分析,从这个函数开始DPVS的收发过程就开始启动,这个函数内容如下
int netif_lcore_start(void) {
rte_eal_mp_remote_launch(netif_loop, NULL, SKIP_MASTER);
return EDPVS_OK;
}
可以看到同样调用的是rte_eal_mp_remote_launch函数设置slave lcore的lcore_config结构体中的lcore_function_t值来让其执行netif_loop函数,启动方式和dpdk的l2fwd也是一样的,来看一下netif_loop函数的主要内容,先删掉用于调试的log信息
static int netif_loop(void *dummy)
{
struct netif_lcore_loop_job *job;
lcoreid_t cid = rte_lcore_id();// 获取自己的lcore id
……
#ifdef DPVS_MAX_LCORE
if (cid >= DPVS_MAX_LCORE)
return EDPVS_IDLE;
#endif
assert(LCORE_ID_ANY != cid);
// 某个核可能专用来收取包,死循环,如果没有配置那么继续
try_isol_rxq_lcore_loop();
if (0 == lcore_conf[lcore2index[cid]].nports) {
RTE_LOG(INFO, NETIF, "[%s] Lcore %d has nothing to do.\n", __func__, cid);
return EDPVS_IDLE;
}
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_INIT], list) {
do_lcore_job(job);
}
while (1) {
……
// 运行所有 NETIF_LCORE_JOB_LOOP 类型的任务
lcore_stats[cid].lcore_loop++; // 记录循环的次数
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_LOOP], list) {
do_lcore_job(job);
}
++netif_loop_tick[cid];
// 慢速任务 NETIF_LCORE_JOB_SLOW
list_for_each_entry(job, &netif_lcore_jobs[NETIF_LCORE_JOB_SLOW], list) {
if (netif_loop_tick[cid] % job->skip_loops == 0) {
do_lcore_job(job);
//netif_loop_tick[cid] = 0;
}
}
……
}
return EDPVS_OK;
}
如果配置了一些lcore是专职用于接收消息的,那么就会调用try_isol_rxq_lcore_loop()函数,然后就会进入一个while(1)的死循环中,首先会执行list_for_each_entry宏,这个宏定义如下
/**
* list_for_each_entry - iterate over list of given type
* @pos: the type * to use as a loop cursor.
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*/
#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
&pos->member != (head); \ // pos指向每一个member
pos = list_next_entry(pos, member))
可以看到这个宏主要是用来迭代一个list的,head是list的头部,member是这个list中的成员变量,然后让pos调用list_next_entry不断指向下一个member,这个pos的变量类型就是netif_lcore_loop_job,然后调用do_lcore_job函数来执行每一个job,可以看到这里对job也是由分类的,每一类job都会对应一个list,然后迭代来执行,dpvs中由如下三类job类型
/**************************** lcore loop job ****************************/
enum netif_lcore_job_type { // 三类job任务
NETIF_LCORE_JOB_INIT = 0,
NETIF_LCORE_JOB_LOOP = 1,
NETIF_LCORE_JOB_SLOW = 2,
NETIF_LCORE_JOB_TYPE_MAX = 3,
};
do_lcore_job函数的主要内容为
static inline void do_lcore_job(struct netif_lcore_loop_job *job)
{
……
job->func(job->data); // 调用job执行的function
……
}
也就是执行job结构体所指向的func函数,入参是job指向的data,通过这样的方式就开始了dpvs的报文收发的过程,而不同类型的job是在main中的netif_init函数中注册的
int netif_init(const struct rte_eth_conf *conf){
cycles_per_sec = rte_get_timer_hz();
netif_pktmbuf_pool_init(); // 初始化mbuf pool
netif_arp_ring_init(); // 二层数据包环形数组初始化
netif_pkt_type_tab_init(); // 不同类型数据包处理函数表的初始化
netif_lcore_jobs_init(); //初始化 jobs 数组
// use default port conf if conf=NULL
netif_port_init(conf); // 初始化端口
netif_lcore_init();// 注册job
return EDPVS_OK;
}
netif_lcore_jobs_init()用于初始化jobs数组
static inline void netif_lcore_jobs_init(void){
int ii;
for (ii = 0; ii < NETIF_LCORE_JOB_TYPE_MAX; ii++) {
INIT_LIST_HEAD(&netif_lcore_jobs[ii]);
}
}
可以看到netif_lcore_jobs_init()就是遍历了3类job然后通过INIT_LIST_HEAD宏,将netif_lcore_jobs[ii]的pre和next指针都指向自己,也就是为每一个netif_lcore_jobs[ii]初始化头部,结合刚才的job的迭代过程,可以看出来job的数据结构是这样的
也就是一个数组,数组的每一个元素都是链表的头部,头部连接了后续的元素,后边可以看到dpvs用了很多这样的数据结构来存储数据,这个存储结构和java里的HashMap的存储结构类似
接下来来看netif_port_init(conf)
/* Allocate and register all DPDK ports available */
inline static void netif_port_init(const struct rte_eth_conf *conf)
{
……
port_tab_init(); // 初始化port_tab数组 数据结构类似job
port_ntab_init();// 初始化port_ntab数组 数据结构类似job
if (!conf)
conf = &default_port_conf; //rte_eth_conf
this_eth_conf = *conf;
// 初始化kni模块 用于将不关心的网络数据包透传到内核
rte_kni_init(NETIF_MAX_KNI);
kni_init();
for (pid = 0; pid < nports; pid++) {
/* queue number will be filled on device start */
port = netif_rte_port_alloc(pid, 0, 0, &this_eth_conf); // 分配内存 设置队列 设置结构体name mac type等字段的值
if (!port)
rte_exit(EXIT_FAILURE, "Port allocate fail, exiting...\n");
if (netif_port_register(port) < 0) // 加入到port_tab和port_ntab数组中
rte_exit(EXIT_FAILURE, "Port register fail, exiting...\n");
}
if (relate_bonding_device() < 0)
rte_exit(EXIT_FAILURE, "relate_bonding_device fail, exiting...\n");
/* auto generate KNI device for all build-in
* phy ports and bonding master ports, but not bonding slaves */
for (pid = 0; pid < nports; pid++) {
port = netif_port_get(pid);
assert(port);
// 每个网卡还要增加 kni 网卡接口,比如原来是 dpdk0, 那么 kni 就叫 dpdk0.kni
if (port->type == PORT_TYPE_BOND_SLAVE)
continue;
kni_name = find_conf_kni_name(pid);
/* it's ok if no KNI name (kni_name is NULL) */
if (kni_add_dev(port, kni_name) < 0)
rte_exit(EXIT_FAILURE, "add KNI port fail, exiting...\n");
}
}
首先会初始化port_tab和port_ntab数组,结构和job类似,用于后边快速获取各个port的信息,netif_rte_port_alloc会为每一个端口分配内存,设置port的netif_port结构体中的一些字段的值,后边用netif_port_register函数将port加入到port_tab和port_ntab数组,最后会为每一个port添加kni的网卡接口,然后来看netif_lcore_init函数,这个函数会初始化每一个lcore,也就是设置lcore的lcore_conf结构体,会配置每一个lcore所管理的端口等,来看后边关键的注册job的部分
#define NETIF_JOB_COUNT 3
struct netif_lcore_loop_job netif_jobs[NETIF_JOB_COUNT];
static void netif_lcore_init(void){
……
/* register lcore jobs*/
snprintf(netif_jobs[0].name, sizeof(netif_jobs[0].name) - 1, "%s", "recv_fwd");
netif_jobs[0].func = lcore_job_recv_fwd; // 收发数据包的job
netif_jobs[0].data = NULL;
netif_jobs[0].type = NETIF_LCORE_JOB_LOOP;
snprintf(netif_jobs[1].name, sizeof(netif_jobs[1].name) - 1, "%s", "xmit");
netif_jobs[1].func = lcore_job_xmit; // 发送网卡数据包
netif_jobs[1].data = NULL;
netif_jobs[1].type = NETIF_LCORE_JOB_LOOP;
snprintf(netif_jobs[2].name, sizeof(netif_jobs[2].name) - 1, "%s", "timer_manage");
netif_jobs[2].func = lcore_job_timer_manage; // 定时器管理
netif_jobs[2].data = NULL;
netif_jobs[2].type = NETIF_LCORE_JOB_LOOP;
for (ii = 0; ii < NETIF_JOB_COUNT; ii++) {
res = netif_lcore_loop_job_register(&netif_jobs[ii]); // 注册每个job
if (res < 0) {
rte_exit(EXIT_FAILURE,
"[%s] Fail to register netif lcore jobs, exiting ...\n", __func__);
break;
}
}
}
可以看到,这里注册的3个类型是NETIF_LCORE_JOB_LOOP的job,分别为lcore_job_recv_fwd,lcore_job_xmit和lcore_job_timer_manage,通过函数指针指向了这三个函数,这三个函数也就实现了dpvs的收发报文的过程,最后通过netif_lcore_loop_job_register将这三个NETIF_LCORE_JOB_LOOP的job添加到netif_lcore_jobs数组中索引为1的地方,这样在int netif_loop中调用do_lcore_job函数的时候,就可以调用到在这里注册的三个函数了
因为是按照来存储每一个job的,所以job的执行也是按照注册的顺序来执行的,因此在死循环里,首先执行的是lcore_job_recv_fwd函数
static void lcore_job_recv_fwd(void *arg){
int i, j;
portid_t pid;
lcoreid_t cid;
struct netif_queue_conf *qconf;
cid = rte_lcore_id(); // 当前的coreid
assert(LCORE_ID_ANY != cid);
// 一个核可能负责多个网卡的多个队列,所以是两个 for
// i 是指网卡号 j 是指 第 i 个网卡的第 j 的队列
// 主动轮询每一个核心管理的每一个端口的每一个队列
for (i = 0; i < lcore_conf[lcore2index[cid]].nports; i++) { // i是网卡号
pid = lcore_conf[lcore2index[cid]].pqs[i].id;
assert(pid <= bond_pid_end);
for (j = 0; j < lcore_conf[lcore2index[cid]].pqs[i].nrxq; j++) { // j是一个收队列
qconf = &lcore_conf[lcore2index[cid]].pqs[i].rxqs[j]; // cid核心的第i个网卡的第j个队列
lcore_process_arp_ring(qconf, cid); // 先检查全局 arp_ring 环形数组是否有数据,如果有就处理
lcore_process_redirect_ring(qconf, cid);
qconf->len = netif_rx_burst(pid, qconf); // 接收网卡数据的核心函数 len是收到的数据包的数量
lcore_stats_burst(&lcore_stats[cid], qconf->len); // core的状态数据的更新
lcore_process_packets(qconf, qconf->mbufs, cid, qconf->len, 0); // 不是来自arp ring的
kni_send2kern_loop(pid, qconf);
}
}
}
这里主要是两个循环,执行这个job也是由一个lcore来执行的,首先会获取当前lcore的cid = rte_lcore_id(),然后通过外循环遍历当前这个core所管理的所有的port,内循环则是遍历当前port的收队列,对于每一个收队列,首先会检查全局 arp_ring 环形数组是否有数据,如果有就处理,然后就是接收数据的核心函数netif_rx_burst
static inline uint16_t netif_rx_burst(portid_t pid, struct netif_queue_conf *qconf){
struct rte_mbuf *mbuf;
int nrx = 0;
// 是否是专职接收数据的队列
if (qconf->isol_rxq) {
……
} else {
nrx = rte_eth_rx_burst(pid, qconf->id, qconf->mbufs, NETIF_MAX_PKT_BURST); // 网卡取出数据 放入到这个port的qconf中的mbunf中
}
qconf->len = nrx; // nrx是包的数量
return nrx;
}
这个函数的关键就是调用rte_eth_rx_burst函数,将收到的数据包放到了qconf的mbufs中,这个过程和l2fwd也是一样的,然后返回到lcore_job_recv_fwd函数中,通过lcore_stats_burst函数来更新当前core的相关数据(收包数等),然后就开始了二层包处理lcore_stats_burst
void lcore_process_packets(struct netif_queue_conf *qconf, struct rte_mbuf **mbufs,
lcoreid_t cid, uint16_t count, bool pkts_from_ring) {
……
for (i = 0; i < count; i++) {
……
eth_hdr = rte_pktmbuf_mtod(mbuf, struct ether_hdr *); // 一个mbuf存着一个数据包 通过这个函数解析出来二层头部 rte_pktmbuf_mtod从头部取数据cast成ether_hdr
/* reuse mbuf.packet_type, it was RTE_PTYPE_XXX */
mbuf->packet_type = eth_type_parse(eth_hdr, dev); // 判断packet的类型 一般都是本机DR模式虚拟IP对应的mac地址就是本机mac
//kni 模式所有的包都要透传到内核,深考贝一份
if (dev->flag & NETIF_PORT_FLAG_FORWARD2KNI) {
……
}
if (eth_hdr->ether_type == htons(ETH_P_8021Q) ||
mbuf->ol_flags & PKT_RX_VLAN_STRIPPED) {
……
}
/* handler should free mbuf */
netif_deliver_mbuf(mbuf, eth_hdr->ether_type, dev, qconf,
(dev->flag & NETIF_PORT_FLAG_FORWARD2KNI) ? true:false,
cid, pkts_from_ring);
lcore_stats[cid].ibytes += mbuf->pkt_len;
lcore_stats[cid].ipackets++;
}
}
函数主体也是一个循环,会遍历收到的数据包,用rte_pktmbuf_mtod解析出来链路层的头部,并且根据情况会走kni处理的分支,或者是vlan处理的分支,我们主要看的是netif_deliver_mbuf函数
static inline int netif_deliver_mbuf(……) // 是否来自arp ring
{
struct pkt_type *pt;
……
pt = pkt_type_get(eth_type, dev); // arp 或者 ip
……
// arp数据包复制给所有的队列
if (pt->type == rte_cpu_to_be_16(ETHER_TYPE_ARP) && !pkts_from_ring) { // 如果是arp类型并且不是来自arp ring的
……
}
mbuf->l2_len = sizeof(struct ether_hdr); // l2的长度
/* Remove ether_hdr at the beginning of an mbuf */
data_off = mbuf->data_off; // 获取data off
if (unlikely(NULL == rte_pktmbuf_adj(mbuf, sizeof(struct ether_hdr))))
return EDPVS_INVPKT;
err = pt->func(mbuf, dev); // 交给三层处理
……
return EDPVS_OK;
}
首先会调用pkt_type_get函数来获取二层数据包的类型,包括arp和ip返回的结果是一个pkt_type结构体,后边会将arp数据包复制给所有的队列,这么做的原因后边再探索,最后会调用pt指向的一个函数来进行三层处理,mbuf就是数据包(此时指向了三层数据),dev就是收到这个数据包的端口,这里的调用方式和之前执行job时候的方式很类似,看到这里有点懵,pt指向的这个处理函数又是在哪里注册的呢,只能返回回去再找,回到main函数里的netif_init函数
int netif_init(const struct rte_eth_conf *conf)
{
cycles_per_sec = rte_get_timer_hz();
netif_pktmbuf_pool_init(); // 初始化mbuf pool同样调用的是 rte_pktmbuf_pool_create 函数
netif_arp_ring_init(); // 二层数据包环形数组初始化
netif_pkt_type_tab_init(); // 不同类型数据包处理函数表的初始化
netif_lcore_jobs_init(); //初始化 jobs 数组
// use default port conf if conf=NULL
netif_port_init(conf); // 初始化端口
netif_lcore_init(); // 注册job
return EDPVS_OK;
}
这里有一个netif_pkt_type_tab_init()函数,根据名字大概也可以判断出来时和pkt_type这个结构体有关系的
static inline void netif_pkt_type_tab_init(void)
{
int i;
for (i = 0; i < NETIF_PKT_TYPE_TABLE_BUCKETS; i++)
INIT_LIST_HEAD(&pkt_type_tab[i]);
}
可以看到这个函数是对pkt_type_tab操作的其结构和刚开始的job类似,并设置了每一个索引上的头部,这里只是设置了头部,并没有设置它的内容,于是继续寻找,在main函数中有一个inet_init函数,这里看到会有不同类型的三层数据包相关的init函数
int inet_init(void)
{
……
if ((err = ipv4_init()) != 0)
return err;
……
return EDPVS_OK;
}
关键来看一下ipv4_init()
int ipv4_init(void){
……
if ((err = ipv4_frag_init()) != EDPVS_OK)
return err;
ip4_pkt_type.type = htons(ETHER_TYPE_IPv4);
if ((err = netif_register_pkt(&ip4_pkt_type)) != EDPVS_OK) {
ipv4_frag_term();
return err;
}
return EDPVS_OK;
}
这里可以看到,最后会调用netif_register_pkt函数,这里可以看到它传入了ip4_pkt_type,而这个变量就是pkt_type结构体,它是这样定义的
static struct pkt_type ip4_pkt_type = {
//.type = rte_cpu_to_be_16(ETHER_TYPE_IPv4),
.func = ipv4_rcv,
.port = NULL,
};
在来看一下netif_register_pkt函数的内容
int netif_register_pkt(struct pkt_type *pt)
{
……
list_add_tail(&pt->list, &pkt_type_tab[hash]);
return EDPVS_OK;
}
这里最后会调用list_add_tail宏定义,可以看的出来它把ip4_pkt_type这个结构体加入到了pkt_type_tab当中,我们再返回到netif_deliver_mbuf函数,这里边调用了pkt_type_get函数
static struct pkt_type *pkt_type_get(uint16_t type, struct netif_port *port){
struct pkt_type *pt;// 数据包类型
int hash;
hash = pkt_type_tab_hashkey(type);
list_for_each_entry(pt, &pkt_type_tab[hash], list) {
if (pt->type == type && ((pt->port == NULL) || pt->port == port)) {
return pt;
}
}
return NULL;
}
发现原来pt会指向pkt_type_tab[hash]这个数组中的元素而这个数组又是在inet_init中添加不同的元素的,于是明白了,pt最后会根据指向ip4_pkt_type这个结构体,而这个结构体的func指针指向的是ipv4_rcv函数,所以接下来的处理会在ipv4_rcv中进行
static int ipv4_rcv(struct rte_mbuf *mbuf, struct netif_port *port) // mbuf 和 端口
{
struct ipv4_hdr *iph; // ipv4的头部
uint16_t hlen, len;
eth_type_t etype = mbuf->packet_type; /* FIXME: use other field ? */
……
//判断包类型是否是本地的,不是就丢弃
IP4_UPD_PO_STATS(in, mbuf->pkt_len);
if (mbuf_may_pull(mbuf, sizeof(struct ipv4_hdr)) != 0)
goto inhdr_error;
iph = ip4_hdr(mbuf); // 获取头部
hlen = ip4_hdrlen(mbuf); // 头部长度
if (((iph->version_ihl) >> 4) != 4 || hlen < sizeof(struct ipv4_hdr)) // 头部长度版本号等数据是正确的
goto inhdr_error;
if (mbuf_may_pull(mbuf, hlen) != 0)
goto inhdr_error;
if (unlikely(!(port->flag & NETIF_PORT_FLAG_RX_IP_CSUM_OFFLOAD))) {
if (unlikely(rte_raw_cksum(iph, hlen) != 0xFFFF)) // 校验和
goto csum_error;
}
len = ntohs(iph->total_length);// 总长度
if (mbuf->pkt_len < len) {
IP4_INC_STATS(intruncatedpkts);
goto drop;
} else if (len < hlen)
goto inhdr_error;
/* trim padding if needed */ //去掉填充
if (mbuf->pkt_len > len) {
……
}
mbuf->userdata = NULL;
mbuf->l3_len = hlen;
……
return INET_HOOK(AF_INET, INET_HOOK_PRE_ROUTING,
mbuf, port, NULL, ipv4_rcv_fin);
……
}
可以看到这个函数开始会检测数据包头部版本号、长度以及校验和等信息,出错以后会直接用goto进入到错误数据包的处理流程,对于正确的数据包会调用INET_HOOK函数
int INET_HOOK(int af, unsigned int hook, struct rte_mbuf *mbuf, // AF INET
struct netif_port *in, struct netif_port *out,
int (*okfn)(struct rte_mbuf *mbuf)) {
struct list_head *hook_list;
struct inet_hook_ops *ops;
struct inet_hook_state state;
int verdict = INET_ACCEPT;
……
hook_list = af_inet_hooks(af, hook); // 回调函数数组
ops = list_entry(hook_list, struct inet_hook_ops, list);
if (!list_empty(hook_list)) { // 遍历回调函数数组
verdict = INET_ACCEPT;
list_for_each_entry_continue(ops, hook_list, list) {
repeat:
verdict = ops->hook(ops->priv, mbuf, &state); // 根据verdict判断后续操作
if (verdict != INET_ACCEPT) {
if (verdict == INET_REPEAT)
goto repeat;
break;
}
}
}
……
}
可以看到,这里主要是一个goto实现的循环,后续的执行都是根据verdict来确定的,可以看的出来,如果verdict值是INET_REPEAT这的时候利用goto会继续执行当前的hook函数,而不是执行下一个hook,否侧会迭代到下一个hook来执行,这里函数调用又是ops->hook这种函数回调的方式,可以看到ops = list_entry(hook_list, struct inet_hook_ops, list)会指向hook_list,而hook_list又是通过af_inet_hooks函数返回的
static inline struct list_head *af_inet_hooks(int af, size_t num){
assert((af == AF_INET || af == AF_INET6) && num < INET_HOOK_NUMHOOKS);
if (af == AF_INET)
return &inet_hooks[num];
else
return &inet6_hooks[num];
}
这个函数中返回的又是一个全局的inet_hooks数组中的一个元素,因此从前边继续寻找inet_hooks数组是在哪里初始化的,再回到main函数里的inet_init函数
int inet_init(void)
{……
if ((err = inet_hook_init()) != 0)
return err;
……
return EDPVS_OK;
}
可以看到这里有一个inet_hook_init函数
static int inet_hook_init(void)
{
……
for (i = 0; i < NELEMS(inet_hooks); i++)
INIT_LIST_HEAD(&inet_hooks[i]);
rte_rwlock_write_unlock(&inet_hook_lock);
……
return EDPVS_OK;
}
可以看到在这个函数里对inet_hooks数组的头部进行了设置,这里还没有在这个数组中添加任何内容,回到main 函数中继续寻找,这里有dp_vs_init()函数
int dp_vs_init(void)
{
//其他的注册操作
……
//对hook进行注册
err = inet_register_hooks(dp_vs_ops, NELEMS(dp_vs_ops));
if (err != EDPVS_OK) {
RTE_LOG(ERR, IPVS, "fail to register hooks: %s\n", dpvs_strerror(err));
goto err_hooks;
}
……
}
这个函数中也是有很多其他注册操作的,后边遇到的话再说,这里先看dp_vs_init()函数最后的inet_register_hooks函数,这个函数传入了dp_vs_ops参数,而这个参数的类型就是inet_hook_ops和前边ops变量类型是一样的而这个dp_vs_ops的内容为
static struct inet_hook_ops dp_vs_ops[] = { // hook的相关操作
{
.af = AF_INET,
.hook = dp_vs_in,
.hooknum = INET_HOOK_PRE_ROUTING,
.priority = 100,
},
{
.af = AF_INET,
.hook = dp_vs_pre_routing,
.hooknum = INET_HOOK_PRE_ROUTING,
.priority = 99,
},
……
};// AF的类型hook到不同的函数 优先级越小越先执行
可以看到是在这里设置了相关的hook,并且每个hook也是有优先级的而inet_register_hooks函数也就是将dp_vs_ops添加到inet_hooks数组中的,inet_register_hooks函数内部又调用了__inet_register_hooks函数
static int __inet_register_hooks(struct list_head *head,
struct inet_hook_ops *reg)
{
……
list_for_each_entry(elem, head, list) {
if (reg->priority < elem->priority)
break;
}
list_add(®->list, elem->list.prev);
return EDPVS_OK;
}
可以看到在添加hook的时候是按照优先级添加的,priority值小的hook会添加到链表的前边,所以再次回到INET_HOOK函数处,那么verdict = ops->hook会回调dp_vs_pre_routing函数这个函数主要是处理,这个函数又会调用__dp_vs_pre_routing函数,而这个函数看上边的注释主要是丢弃ip分片,丢弃发送到tcp-vip的udp包和开启syn proxy来防止syn洪范攻击,和DR模式的主要处理流程没太大关系,所以这次先跳过,那么下一个verdict = ops->hook执行的函数是dp_vs_in,这个函数调用了__dp_vs_in,从这里开始才真正进入到了lvs模块
static int __dp_vs_in(void *priv, struct rte_mbuf *mbuf,
const struct inet_hook_state *state, int af)
{
struct dp_vs_iphdr iph; // 头部
struct dp_vs_proto *prot; // 判断协议的结构体 不同协议会有不同结构体
struct dp_vs_conn *conn; // 判断属于哪个连接的结构体
……
if (dp_vs_fill_iphdr(af, mbuf, &iph) != EDPVS_OK) // 填充iph头部
return INET_ACCEPT;
// 处理icmp
if (unlikely(iph.proto == IPPROTO_ICMP ||
iph.proto == IPPROTO_ICMPV6)) {
……
}
prot = dp_vs_proto_lookup(iph.proto); // 根据iph的头部的协议字段 找到合适的dp_vs_proto结构体
// 丢掉frag
if (af == AF_INET && ip4_is_frag(ip4_hdr(mbuf))) {
……
}
// 判断这个数据包是否属于已有的一个连接
conn = prot->conn_lookup(prot, &iph, mbuf, &dir, false, &drop, &peer_cid); // 方向写入到dir中
……
// 没有connection连接 新来的数据包
if (unlikely(!conn)) {
if (prot->conn_sched(prot, &iph, mbuf, &conn, &verdict) != EDPVS_OK) {
/* RTE_LOG(DEBUG, IPVS, "%s: fail to schedule.\n", __func__); */
return verdict;
}
……
// 如果connection是存在的
if (conn->flags & DPVS_CONN_F_SYNPROXY) {……}
这个函数首先根据数据包的协议用dp_vs_proto_lookup函数返回dp_vs_proto结构体,而它内部是通过一个数组来返回的
struct dp_vs_proto *dp_vs_proto_lookup(uint8_t proto){
assert(proto < DPVS_MAX_PROTOS);
return dp_vs_protocols[proto];
}
再次回到main函数,看看这个dp_vs_protocols数组是在哪里初始化的,main函数中的dp_vs_init函数中的dp_vs_proto_init函数,就是对这个数组的初始化
int dp_vs_proto_init(void){
……
if ((err = proto_register(&dp_vs_proto_tcp)) != EDPVS_OK) {
RTE_LOG(ERR, IPVS, "%s: fail to register TCP\n", __func__);
goto tcp_error;
}
……
}
我们主要看tcp的流程,proto_register函数就是将dp_vs_proto_tcp添加到dp_vs_protocols数组
struct dp_vs_proto dp_vs_proto_tcp = {
.name = "TCP",
.proto = IPPROTO_TCP,
.init = tcp_init,
.exit = tcp_exit,
.conn_sched = tcp_conn_sched,
.conn_lookup = tcp_conn_lookup,
.conn_expire = tcp_conn_expire,
.nat_in_handler = tcp_snat_in_handler,
.nat_out_handler = tcp_snat_out_handler,
.fnat_in_handler = tcp_fnat_in_handler,
.fnat_out_handler = tcp_fnat_out_handler,
.snat_in_handler = tcp_snat_in_handler,
.snat_out_handler = tcp_snat_out_handler,
.state_trans = tcp_state_trans,
};
所以现在在__dp_vs_in函数中,prot现在指向了dp_vs_proto_tcp结构体,所以继续往后执行prot->conn_lookup的时候调用的函数就是dp_vs_proto_tcp结构体的tcp_conn_lookup函数
static struct dp_vs_conn * tcp_conn_lookup(……)
{
struct tcphdr *th, _tcph;
struct dp_vs_conn *conn;
……
th = mbuf_header_pointer(mbuf, iph->len, sizeof(_tcph), &_tcph); // 解析出来tcp的头部
……
if (dp_vs_blklst_lookup(iph->proto, &iph->daddr, th->dest, &iph->saddr)) { // 在ip黑名单里的话就过滤掉
*drop = true;
return NULL;
}
conn = dp_vs_conn_get(iph->af, iph->proto,
&iph->saddr, &iph->daddr, th->source, th->dest, direct, reverse); // 获取连接
// 确认邻居子系统
if (conn != NULL) {
……
}
return conn;
}
首先会解析出来tcp的头部,并会过滤掉ip黑名单里的数据包,然后通过dp_vs_conn_get获取到已经存在的连接,获取连接的大致原理是以<地址族, 协议, 源ip, 源端口, 目的ip, 目的端口>作为五元组计算哈希来作为key去查找,最后会把conn返回,然后回到__dp_vs_in中,会判断是否找到了这个conn,如果没有找到的话执行prot->conn_sched,也就是调用了dp_vs_proto_tcp结构体的tcp_conn_sched方法来建立新的连接
static int tcp_conn_sched(……) {
struct tcphdr *th, _tcph;
struct dp_vs_service *svc;
……
th = mbuf_header_pointer(mbuf, iph->len, sizeof(_tcph), &_tcph); // th和_tcph都指向头部
……
//只允许syn包来建立连接
if (!th->syn || th->ack || th->fin || th->rst) {
……// 丢弃非syn包
}
// 只剩下syn数据包
svc = dp_vs_service_lookup(iph->af, iph->proto,// 是否注册了服务 根据vip和端口
&iph->daddr, th->dest, 0, mbuf, NULL);
if (!svc) {
……// 没有在ipvsadm中注册服务直接返回
return EDPVS_NOSERV;
}
*conn = dp_vs_schedule(svc, iph, mbuf, false); // 存在这个服务就创建一个connection
if (!*conn) {
dp_vs_service_put(svc);
*verdict = INET_DROP;
return EDPVS_RESOURCE;
}
dp_vs_service_put(svc);
return EDPVS_OK;
}
这个函数首先会过滤掉非syn的数据包,只允许syn包来建立连接,然后调用dp_vs_service_lookup来检测当前数据包是否是请求了已注册的服务,拿虚拟ip和端口进行判断,然后调用dp_vs_schedule函数来建立一个新的连接,这个函数就是选择一个RS并建立一条新的连接
struct dp_vs_conn *dp_vs_schedule(……)
{
uint16_t _ports[2], *ports; /* sport, dport */
struct dp_vs_dest *dest; // RS的结构体
struct dp_vs_conn *conn;
struct dp_vs_conn_param param;
……
if (svc->flags & DP_VS_SVC_F_PERSISTENT) // 长连接的建立
……
dest = svc->scheduler->schedule(svc, mbuf); // 根据特定算法选择rs
……
//SNAT处理
if (dest->fwdmode == DPVS_FWD_MODE_SNAT)
return dp_vs_snat_schedule(dest, iph, ports, mbuf);
// ICMP处理
if (unlikely(iph->proto == IPPROTO_ICMP)) {
……
} else if (unlikely(iph->proto == IPPROTO_ICMPV6)) {
……
} else {
// 填充参数 proto, caddr, vaddr, cport, vport 供新建连接使用
dp_vs_conn_fill_param(iph->af, iph->proto,
&iph->saddr, &iph->daddr,
ports[0], ports[1], 0, ¶m);
}
conn = dp_vs_conn_new(mbuf, iph, ¶m, dest, // dp_vs_conn_new 根据参数 和目标机器信息建立代理连接
is_synproxy_on ? DPVS_CONN_F_SYNPROXY : 0);
if (!conn)
return NULL;
dp_vs_stats_conn(conn);
return conn;
}
先不考虑长连接的建立,这里会执行svc->scheduler->schedule,看来有必要返回回去看看scheduler字段是在哪里初始化的,在main函数中的dp_vs_init函数中的dp_vs_sched_init函数完成了scheduler的初始化
int dp_vs_sched_init(void){
INIT_LIST_HEAD(&dp_vs_schedulers);
rte_rwlock_init(&__dp_vs_sched_lock);
dp_vs_rr_init();
dp_vs_wrr_init();
dp_vs_wlc_init();
dp_vs_conhash_init();
dp_vs_fo_init();
return EDPVS_OK;
}
这里就列出了各种负载均衡的调度算法,最简单的就是dp_vs_rr_init(),这个函数实现了轮询调度算法(Round-Robin Scheduling),轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环(lvs DR模式的笔记中用的就是这个算法),再回到dp_vs_schedule函数中,svc->scheduler->schedule根据负载均衡的算法返回一个dest,dest就对应着一个RS,跳过ICMP和SNAT的处理过程,然后调用dp_vs_conn_fill_param函数,这个函数会给param填充协议, 客户端ip, 虚拟ip, 客户端端口, 虚拟端口等字段,供新建连接使用,最后会调用dp_vs_conn_new函数来创建连接,函数比较长分段来看一下
struct dp_vs_conn *dp_vs_conn_new(……)
{
struct dp_vs_conn *new;
struct dp_vs_redirect *new_r = NULL;
struct conn_tuple_hash *t;
uint16_t rport;
__be16 _ports[2], *ports;
int err;
……
new = dp_vs_conn_alloc(); // 新连接分配内存
……
// 设置RS的端口号
if ((flags & DPVS_CONN_F_TEMPLATE) || param->ct_dport != 0)
……
} else {
// DR 模式直接取dest里的port
rport = dest->port;
}
// 一个连接对应两个conn_tuple_hash 代表两个方向
// conn_tuple_hash connect的一个hash表
t = &tuplehash_in(new); // 入口方向的流量
t->direct = DPVS_CONN_DIR_INBOUND;
t->af = param->af;
t->proto = param->proto;
t->saddr = *param->caddr; // 源地址是 外网 client addr
t->sport = param->cport; // client的port
t->daddr = *param->vaddr; // 目地地址是 服务虚IP地址
t->dport = param->vport; // 虚拟port
INIT_LIST_HEAD(&t->list);
// 出口方向的流量
t = &tuplehash_out(new);
t->direct = DPVS_CONN_DIR_OUTBOUND;
t->af = dest->af;
t->proto = param->proto;
if (dest->fwdmode == DPVS_FWD_MODE_SNAT) {
t->saddr = iph->saddr;
} else {
t->saddr = dest->addr; // 非SNAT 就是RS的ip
}
t->sport = rport;
t->daddr = *param->caddr; /* non-FNAT */
t->dport = param->cport; /* non-FNAT */
INIT_LIST_HEAD(&t->list);
首先会设置RS目的端口,在DR中目的RS的目的端口就是当时注册服务时候的目的端口,也就是dest的目的端口,然后每一个连接都会对应两个conn_tuple_hash,对应着连接的进出两个方向,进方向就是从Client->DPVS,出方向就是DPVS->RS,这个函数前半段就是对者两个方向的conn_tuple_hash进行赋值,可以看到主要是对两个方向的源地址目的地址等赋值,包括IP和端口,函数的后半段就是创建一个新的connection,并对相关字段赋值
/* init connection */
new->af = param->af;
new->proto = param->proto;
new->caddr = *param->caddr;
new->cport = param->cport;
new->vaddr = *param->vaddr;
new->vport = param->vport;
new->laddr = *param->caddr; /* non-FNAT */
new->lport = param->cport; /* non-FNAT */
……
err = conn_bind_dest(new, dest); // 根据转发模式绑定转发操作的函数
……
/* timer */
new->timeout.tv_sec = conn_init_timeout;
new->timeout.tv_usec = 0; // 超时设定
// syn proxy的设置
INIT_LIST_HEAD(&new->ack_mbuf);
rte_atomic32_set(&new->syn_retry_max, 0);
rte_atomic32_set(&new->dup_ack_cnt, 0);
if ((flags & DPVS_CONN_F_SYNPROXY) && !(flags & DPVS_CONN_F_TEMPLATE)) {
……
}
/* schedule conn timer */
dpvs_time_rand_delay(&new->timeout, 1000000);
if (new->flags & DPVS_CONN_F_TEMPLATE) // 连接加入到 超时管理
dpvs_timer_sched(&new->timer, &new->timeout, conn_expire, new, true);
else
dpvs_timer_sched(&new->timer, &new->timeout, conn_expire, new, false);
……
return new;
……
跳过其他转发模式,只看DR模式的处理流程,除了对源和目的地址的设置外,还会调用conn_bind_dest函数,这个函数就是根据LB的转发模式来绑定相应的转发函数
static int conn_bind_dest(struct dp_vs_conn *conn, struct dp_vs_dest *dest) //在这里设置转发模式相关的几个发包收包操作
{
……
switch (dest->fwdmode) {
case DPVS_FWD_MODE_NAT:
conn->packet_xmit = dp_vs_xmit_nat;
conn->packet_out_xmit = dp_vs_out_xmit_nat;
break;
……
case DPVS_FWD_MODE_DR:
conn->packet_xmit = dp_vs_xmit_dr; // DR模式 只有客户端请求的流量进来然后转发,所以只绑定了一个操作
break;
……
}
conn->dest = dest;
return EDPVS_OK;
}
这里也可以看到与nat模式相比,DR只绑定了一个转发函数,因为DR模式DPVS只处理从客户端流入的流量,而nat模式需要处理两个方向的流量,所以nat需要绑定两个处理函数,先不看绑定的转发函数的内容继续往后看,执行完conn_bind_dest函数后,会设置新建的connection的超时等信息,然后将其加入到超时管理中,最后将新建立的连接返回,又回到了dp_vs_schedule函数中,然后会调用dp_vs_stats_conn函数,这个函数就是将这个connection所属的lcore的连接数量加1,最后将这个connection返回,回到了tcp_conn_sched函数中,然后就会返回EDPVS_OK(0)值给__dp_vs_in函数,于是就执行到了这里
/* try schedule RS and create new connection */
if (prot->conn_sched(prot, &iph, mbuf, &conn, &verdict) != EDPVS_OK) {
/* RTE_LOG(DEBUG, IPVS, "%s: fail to schedule.\n", __func__); */
return verdict;
}
由于返回值是EDPVS_OK,所以会继续向后执行,这个时候conn已经被赋值
// 如果connection是存在的
if (conn->flags & DPVS_CONN_F_SYNPROXY) { // syn proxy
……
}
……
/* holding the conn, need a "put" later. */
if (dir == DPVS_CONN_DIR_INBOUND) // 输入方向
return xmit_inbound(mbuf, prot, conn); // DR模式只有这一个方向
else
return xmit_outbound(mbuf, prot, conn);
最后会执行并返回xmit_inbound函数,因为DR模式只有这一个方向,所以DR模式会走这个分支
static int xmit_inbound(struct rte_mbuf *mbuf, struct dp_vs_proto *prot, struct dp_vs_conn *conn){
……
/* forward to RS */
err = conn->packet_xmit(prot, conn, mbuf);
……
return INET_STOLEN;
}
可以看到最后会执行conn-> packet_xmit,也就是当时conn创建的时候通过conn_bind_dest函数绑定的转发函数,先不看这个绑定函数的内容,我们可以看到执行完conn->packet_xmit后,如果没有出错的话会返回INET_STOLEN,这个值最后会返回到开始的INET_HOOK函数里,到了这个位置
if (!list_empty(hook_list)) { // 遍历回调函数数组
verdict = INET_ACCEPT;
list_for_each_entry_continue(ops, hook_list, list) {
repeat:
verdict = ops->hook(ops->priv, mbuf, &state); // 根据verdict判断后续操作
if (verdict != INET_ACCEPT) {
if (verdict == INET_REPEAT) // 重复操作而不是调用下一个hook
goto repeat;
break;
}
}
}
由于这个返回值是INET_STOLEN不等于INET_ACCEPT,这样会继续迭代hook_list直到迭代完毕为止,现在回过头来看当时connection绑定的函数的处理逻辑
int dp_vs_xmit_dr(struct dp_vs_proto *proto, struct dp_vs_conn *conn, struct rte_mbuf *mbuf){
int af = conn->af;
assert(af == AF_INET || af == AF_INET6);
return af == AF_INET ? __dp_vs_xmit_dr4(proto, conn, mbuf)
: __dp_vs_xmit_dr6(proto, conn, mbuf);
}
来看ipv4的处理逻辑__dp_vs_xmit_dr4
static int __dp_vs_xmit_dr4(struct dp_vs_proto *proto, struct dp_vs_conn *conn, struct rte_mbuf *mbuf) {
……
rt = route4_output(&fl4);
if (!rt) {
err = EDPVS_NOROUTE;
goto errout;
}
……
mbuf->packet_type = ETHER_TYPE_IPv4;
err = neigh_output(AF_INET, (union inet_addr *)&conn->daddr.in, mbuf, rt->port); //dr工作在二层网络中,最终调用 neigh_output 邻居子系统,将包发出去
route4_put(rt);
return err;
……
}
由于DR工作在二层网络中,会调用neigh_output函数,可以看到二层转发是用neigh_table数组来存储RS的相关信息,关于邻居子系统这一块这次先不分析
int neigh_output(int af, union inet_addr *nexhop, struct rte_mbuf *m, struct netif_port *port)
{
……
hashkey = neigh_hashkey(af, nexhop, port);
neighbour = neigh_lookup_entry(af, nexhop, port, hashkey);
if (neighbour) {
if ((neighbour->state == DPVS_NUD_S_NONE) || (neighbour->state == DPVS_NUD_S_SEND)) {
……
} else if ((neighbour->state == DPVS_NUD_S_REACHABLE) || (neighbour->state == DPVS_NUD_S_PROBE) || (neighbour->state == DPVS_NUD_S_DELAY)) {
neigh_fill_mac(neighbour, m, NULL, port); // 填充mac地址
netif_xmit(m, neighbour->port); // 发送出去
if (neighbour->state == DPVS_NUD_S_PROBE) {
neigh_state_confirm(neighbour);
neigh_entry_state_trans(neighbour, 0);
}
return EDPVS_OK;
}
return EDPVS_IDLE;
}
else{
……
return EDPVS_OK;
}
}
最后调用的是neigh_fill_mac(neighbour, m, NULL, port)和netif_xmit(m, neighbour->port),其中neigh_fill_mac函数内容如下
static void neigh_fill_mac(struct neighbour_entry *neighbour, struct rte_mbuf *m, const struct in6_addr *target, struct netif_port *port){
……
eth = (struct ether_hdr *)rte_pktmbuf_prepend(m, (uint16_t)sizeof(struct ether_hdr)); //eth 指向数据包的头部
if (!neighbour && target) {
ipv6_mac_mult(target, &mult_eth);
ether_addr_copy(&mult_eth, ð->d_addr);
} else {
ether_addr_copy(&neighbour->eth_addr, ð->d_addr); // 目的mac改成RS的mac
}
ether_addr_copy(&port->addr, ð->s_addr); // 源mac改成DPVS发出的端口的mac
……
}
可以看到这里是把数据包的目的mac该成RS的mac地址,而把源mac改成DPVS发出的端口的mac,修改完后来看netif_xmit函数
int netif_xmit(struct rte_mbuf *mbuf, struct netif_port *dev){
……
/* assert for possible double free */
mbuf_refcnt = rte_mbuf_refcnt_read(mbuf);
assert((mbuf_refcnt >= 1) && (mbuf_refcnt <= 64));
// 先进行流控
if (dev->flag & NETIF_PORT_FLAG_TC_EGRESS) {
mbuf = tc_handle_egress(netif_tc(dev), mbuf, &ret);
if (likely(!mbuf))
return ret;
}
return netif_hard_xmit(mbuf, dev);
}
可以看到在进行完流控以后会调用netif_hard_xmit函数
int netif_hard_xmit(struct rte_mbuf *mbuf, struct netif_port *dev){
lcoreid_t cid;
int pid, qindex;
struct netif_queue_conf *txq;
struct netif_ops *ops;
……
ops = dev->netif_ops;
if (ops && ops->op_xmit)
return ops->op_xmit(mbuf, dev); // 如果dev的netif_ops的op_xmit不为空的话就执行op_xmit函数
cid = rte_lcore_id();
if (likely(mbuf->ol_flags & PKT_TX_IP_CKSUM))
mbuf->l2_len = sizeof(struct ether_hdr);
if (rte_get_master_lcore() == cid) { // master thread master core 的处理
……
}
……
txq = &lcore_conf[lcore2index[cid]].pqs[port2index[cid][pid]].txqs[qindex];
if (unlikely(txq->len == NETIF_MAX_PKT_BURST)) { // 缓冲区满了直接发送
netif_tx_burst(cid, pid, qindex);
txq->len = 0;
}
lcore_stats[cid].obytes += mbuf->pkt_len;
txq->mbufs[txq->len] = mbuf;
txq->len++;
return EDPVS_OK;
}
这里看到首先会执行if (ops && ops->op_xmit)判断,我们回头看op_xmit是否被注册,回到main函数中的netif_init函数中的netif_port_init函数,这个函数有如下循环
for (pid = 0; pid < nports; pid++) {
port = netif_rte_port_alloc(pid, 0, 0, &this_eth_conf); // 分配内存 设置队列 设置结构体name mac type等字段的值
if (!port)
rte_exit(EXIT_FAILURE, "Port allocate fail, exiting...\n");
if (netif_port_register(port) < 0) // 加入到port_tab 和 port_ntab两个数组中
rte_exit(EXIT_FAILURE, "Port register fail, exiting...\n");
}
查看其中的netif_rte_port_alloc函数
static struct netif_port* netif_rte_port_alloc(portid_t id, int nrxq, int ntxq, const struct rte_eth_conf *conf){
……
if (id >= phy_pid_base && id < phy_pid_end) {
port->type = PORT_TYPE_GENERAL; /* update later in netif_rte_port_alloc */
port->netif_ops = &dpdk_netif_ops;
} else if (id >= bond_pid_base && id < bond_pid_end) {
port->type = PORT_TYPE_BOND_MASTER;
port->netif_ops = &bond_netif_ops;
} else {
RTE_LOG(ERR, NETIF, "%s: invalid port id: %d\n", __func__, id);
rte_free(port);
return NULL;
}
……
return port;
}
可以看到这个函数里会给port的netif_ops字段设置值&dpdk_netif_ops,来看一下这个结构体
static struct netif_ops dpdk_netif_ops = {
.op_set_mc_list = dpdk_set_mc_list,
.op_set_fdir_filt = dpdk_set_fdir_filt,
.op_filter_supported = dpdk_filter_supported,
};
可以看到这个结构体并没有设置op_xmit字段,因此在netif_hard_xmit函数中是不会走if (ops && ops->op_xmit)分支的,继续往后看,后边会判断当前core是否是master core,但是业务逻辑处理是slave core进行的,所以也不会走这个分支,继续往后还会有一个分支,就是如果发送队列缓冲区满的话会执行netif_tx_burst(cid, pid, qindex)函数,从这个函数可以看到它调用了rte_eth_tx_burst,也就是把数据发送出去,如果缓冲区没有满的话,它会从lcore_conf这个全局的数组中找到当前core的端口的发送队列,并把这个消息加到这个队列上,这样等到当前这个job(lcore_job_recv_fwd)执行完以后,会执行下一个job(lcore_job_xmit)
static void lcore_job_xmit(void *args){
int i, j;
lcoreid_t cid;
portid_t pid;
struct netif_queue_conf *qconf;
cid = rte_lcore_id();
//遍历每一个core的每一个端口的每一个发送队列
for (i = 0; i < lcore_conf[lcore2index[cid]].nports; i++) {
pid = lcore_conf[lcore2index[cid]].pqs[i].id;
……
for (j = 0; j < lcore_conf[lcore2index[cid]].pqs[i].ntxq; j++) {
qconf = &lcore_conf[lcore2index[cid]].pqs[i].txqs[j];
if (qconf->len <= 0)
continue;
netif_tx_burst(cid, pid, j);
qconf->len = 0;
}
}
}
这个函数的逻辑很明显了,会遍历lcore_conf数组找到每一个core的每一个端口的每一个发送队列,然后同样会调用netif_tx_burst函数,将队列里缓存的数据发送出去,发送完成后,会把相应队列的长度改成0,这样就走完了整个DR模式的数据包的收发过程。