精华内容
下载资源
问答
  • DisplayPort1.4将支持 8K 分辨率的信号传输,兼容 USB Type-C 接口。从本次更新的技术参数可以看到,这次的eDP 1.4a接口在显示适配器及显示器之间提供4条HBR3高速通道,单通道带宽达到了8.1Gbps,这些通道可独立运行...
  • Virtual Serial Port Driver 7.2英文版 安装说明: 1、运行vspd.exe安装软件。 2、复制vspdctl.dll到安装目录,覆盖原文件。 3、启动vspdconfig.exe Win10/64位系统测试正常使用。
  • WIN10_ PL2303_USB-to-Serial Comm Port驱动下载

    千次下载 热门讨论 2017-04-01 16:15:28
    WIN10_ PL2303_USB-to-Serial Comm Port 驱动以及安装解决方案
  • DisplayPort1.2官方标准

    热门讨论 2014-05-07 16:44:22
    DisplayPort(DP)是目前正在兴起的音视频传输接口,本文档是目前最新的1.2a版本官方标准,支持MST传输技术。
  • 虚拟串口软件Virtual Serial Port Driver最新版8.0.412 支持Windows10 复制patch文件夹下vspdctl.dll至安装文件夹下替换原文件即可
  • 大家从来都没质疑过reuseport说明大家都不在乎reuseport。既然大家都不在乎,那我就来说说大家都不在乎的东西。大家都不管其实不是大家都不管,并不是大家觉得这很牛逼,实际上,真正使用这个机制的公司或者个人,...

    皮鞋,湿而不胖!

    说好的,周末写一篇关于reuseport的。凌晨一点多被正则给吵醒,索性一气之下就起床了,发周报,梳理工作,回答问题,写本文。


    大家从来都没质疑过reuseport说明大家都不在乎reuseport。既然大家都不在乎,那我就来说说大家都不在乎的东西。大家都不管其实不是大家都不管,并不是大家觉得这很牛逼,实际上,真正使用这个机制的公司或者个人,早就偷偷地把它的实现机制给改掉了,只是很少有人公开罢了。

    门槛永远是简单的算法,而不是复杂的工程!

    Linux内核在3.9引入的reuseport的思路是好的,它第一次使得一组socket之间从热备关系变成了负载均衡关系,但是它的实现是垃圾的,不仅仅存在查找socket时的O(n)问题,而且根本就没法实现一致性哈希,这让一组相互负载均衡的进程很难被管理,一损俱损。

    Linux 4.6内核对reuseport进行了重构,解决了O(n)问题,但是依然很难实现一致性哈希。虽然说它自带了bpf的支持,可以从用户态灌入哈希算法实现特殊的socket查找逻辑,但是说实话,这只是提供了另一种获取hash输入的方法,对于保持服务器端socket集群的一致性,几乎没有什么实际的用处。

    关于socket的reuseport,详见我去年写的一篇文章:
    关于Linux UDP/TCP reuseport 二三事: https://blog.csdn.net/dog250/article/details/80458669


    本文以Linux 4.9内核为基础版本来进行实际操作。

    先来看一下reuseport的一致性哈希问题。

    我们实现一个简单UDP服务器程序,用于处理客户端的请求,在本例中,所谓的处理请求仅仅是打印接收到的消息。代码如下:

    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #define PORT	 8080
    #define MAXLINE 1024
    
    int main(int argc, char **argv)
    {
    	int sockfd;
    	char buffer[MAXLINE];
    	struct sockaddr_in server, client;
    	int optval = 1;
    	int len;
    	int ret;
    
    	memset(&server, 0, sizeof(server));
    	memset(&client, 0, sizeof(client));
    
    	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    		perror("socket");
    		return -1;
    	}
    	if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0) {
    		perror("bind");
    		return -1;
    	}
    
    	server.sin_family = AF_INET;
    	server.sin_port = htons(PORT);
    	server.sin_addr.s_addr = INADDR_ANY;
    
    	if (bind(sockfd, (const struct sockaddr *)&server, sizeof(server)) < 0) {
    		perror("bind");
    		return -1;;
    	}
    
    	while (1) {
    		ret = recvfrom(sockfd, (char *)buffer, MAXLINE,
    				MSG_WAITALL, ( struct sockaddr *) &client, &len);
    		buffer[ret] = '\0';
    		printf("recv :%s\n", buffer);
    	}
    
    	return 0;
    }
    

    然后在另一台直连的机器上部署一系列的客户端,简单的持续不断地发送单一的字符串:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #define PORT	 8080
    #define MAXLINE 1024
    
    int main(int argc, char **argv)
    {
    	int sockfd;
    	char *ser = argv[1];
    	char *buff = argv[2];
    	struct sockaddr_in	 server;
    	int ret;
    	int  len;
    
    	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    		perror("socket");
    		return -1;
    	}
    
    	memset(&server, 0, sizeof(server));
    
    	server.sin_family = AF_INET;
    	server.sin_port = htons(PORT);
    	server.sin_addr.s_addr = inet_addr(ser);;
    
    	while (1) {
    		sendto(sockfd, (const char *)buff, strlen(buff),
    			MSG_CONFIRM, (const struct sockaddr *) &server, sizeof(server));
    		printf("%s\n", buff);
    
    		sleep(1);
    	}
    
    	close(sockfd);
    	return 0;
    }
    

    现在让我们在服务器端启动两个server进程,可想而知,它们互为reuseport而负载均衡,同时在客户端启动三个client进程,不断重试变换源端口,直到三个client进程分别被负载在三个不同的server进程,其中client进程的启动方式如下:

    [root@localhost TEST]# ./a.out 172.16.1.2 111 &
    [root@localhost TEST]# ./a.out 172.16.1.2 222 &
    [root@localhost TEST]# ./a.out 172.16.1.2 333 &
    

    然后观测server进程:
    在这里插入图片描述
    非常OK!完美的负载均衡!

    此时,我们把第一个启动的server进程重启,看看会发生什么。第一个启动的server是最左边的那个,我们把它重启:
    在这里插入图片描述
    重启后,第一个server进程和第三个server进程的处理乱掉了,我们重启的是第一个进程,为什么会影响到第三个进程的处理呢?貌似二者相互对调了处理。如果不断有socket重启,那么整个处理关系将全部乱掉。我们很多时候还是希望数据处理可以 保持连接 的!

    这还得看代码,先看socket退出时的处理:
    在这里插入图片描述
    再看socket启动后的处理:
    在这里插入图片描述

    非常简单,当某个socket退出后,最后面的那个socket会接管到退出socket的位置,如果退出的socket重启,那么它将被添加到最后的位置,相当于和接管它的那个socket进行了对调,这就是问题的根源了。

    起初在我没有看reuseport的代码时,我一直以为它是用链表来管理socket集群的,没想到却是使用的预分配的数组,那么如此看来,其内部结构体里面的num_socks字段无非也就是一个计数而已,它的作用仅仅局限于追踪socket集群中socket的数量是0还是非0,如果是0即释放整个数组,仅此而已。

    那么用这个num_socks字段来索引队尾slot中的socket,那就不太合适了,因为它的改变意味着hash取模时模的改变。

    既然Linux内核本身也是使用的数组,索引追踪丝毫起不到节省内存的作用,那我就放心了,我也在这个数组里面折腾呗,实现了一致性哈希,还不用付出空间代价。

    如何来修正这个问题呢?我们希望的是,当退出的socket重启后,一切恢复原样。

    起初,我是准备做一个bpf程序然后注入进去,但是非常麻烦,实际上bpf嵌入到reuseport逻辑里面,那是让你用非默认的五元组来做hash的,比如QUIC取数据报文前面的session ID来做hash等。

    bpf并没有修改当你已经计算出hash值之后的socket选择算法本身,它无非只是修改hash算法的输入而已。

    不那么麻烦了,简单问题不要复杂化。
    在这里插入图片描述


    只要在socket退出的时候,记住退出的位置,新创建socket的时候,不再从最后来pending,而是填充当初记住的那个位置即可!

    思路就是上面这句话,但是实现方案却是多种多样。如果我不想编译内核,希望使用热补丁,那么我也只是需要重写reuseport的几个函数就可以了,值得注意的是,由于不能在结构体添加新的字段,所以可能需要时间换空间了,这意味着在处理性能上要打上一些折扣,不管怎样,实现一个简版再说吧。

    以下是代码:

    int hook_reuseport_add_sock(struct sock *sk, struct sock *sk2)
    {
    	struct sock_reuseport *reuse;
    	int i;
    
    	if (!rcu_access_pointer(sk2->sk_reuseport_cb)) {
    		int err = reuseport_alloc(sk2);
    
    		if (err)
    			return err;
    	}
    
    	spin_lock_bh(lock);
    	reuse = rcu_dereference_protected(sk2->sk_reuseport_cb,
    					  lockdep_is_held(&reuseport_lock)),
    	WARN_ONCE(rcu_dereference_protected(sk->sk_reuseport_cb,
    					    lockdep_is_held(&reuseport_lock)),
    		  "socket already in reuseport group");
    
    	if (reuse->num_socks == reuse->max_socks) {
    		reuse = reuseport_grow(reuse);
    		if (!reuse) {
    			spin_unlock_bh(lock);
    			return -ENOMEM;
    		}
    	}
    
    	for (i = 0; i < reuse->max_socks; i++) {
    		// detach的时候,会将slot设置为NULL。
    		if (reuse->socks[i] == NULL) {
    			reuse->socks[i] = sk;
    			break;
    		}
    	}
    	/* paired with smp_rmb() in reuseport_select_sock() */
    	smp_wmb();
    	reuse->num_socks++;
    	rcu_assign_pointer(sk->sk_reuseport_cb, reuse);
    
    	spin_unlock_bh(lock);
    
    	return 0;
    }
    
    void hook_reuseport_detach_sock(struct sock *sk)
    {
    	struct sock_reuseport *reuse;
    	int i;
    
    	spin_lock_bh(lock);
    	reuse = rcu_dereference_protected(sk->sk_reuseport_cb,
    					  lockdep_is_held(lock));
    	rcu_assign_pointer(sk->sk_reuseport_cb, NULL);
    
    	for (i = 0; i < reuse->max_socks; i++) {
    		// 简单地设置为NULL,后续socket add的时候,检查NULL位即可。
    		// 更好地方法就是从后往前补。
    		if (reuse->socks[i] == sk) {
    			reuse->socks[i] = NULL;
    			reuse->num_socks--;
    			break;
    		}
    	}
    	if (reuse->num_socks == 0)
    		call_rcu(&reuse->rcu, reuseport_free_rcu);
    	spin_unlock_bh(lock);
    }
    
    struct sock *hook_reuseport_select_sock(struct sock *sk, u32 hash, struct sk_buff *skb, int hdr_len)
    {
    	struct sock_reuseport *reuse;
    	struct bpf_prog *prog;
    	struct sock *sk2 = NULL;
    	u16 socks;
    	int i = 0, j = 0, idx = 0;
    
    	rcu_read_lock();
    	reuse = rcu_dereference(sk->sk_reuseport_cb);
    
    	/* if memory allocation failed or add call is not yet complete */
    	if (!reuse)
    		goto out;
    
    	prog = rcu_dereference(reuse->prog);
    	socks = READ_ONCE(reuse->max_socks);
    	j = socks;
    	if (likely(socks)) {
    		/* paired with smp_wmb() in reuseport_add_sock() */
    		smp_rmb();
    
    		if (prog && skb)
    			sk2 = run_bpf(reuse, socks, prog, skb, hdr_len);
    		else {
    			// 取模时需要基于最大的socket索引,这样才能保证一致性。
    			// 因此,需要找到最高的socket索引。
    			j = reuse->max_socks;
    			while (!reuse->socks[j-1]) {
    				j = j - 1;
    			}
    			idx = reciprocal_scale(hash, j);
    			sk2 = reuse->socks[idx];
    		}
    	}
    	i = 0;
    	// 如果hash到了一个NULL位置,那么就取下一个非NULL的slot中的socket。
    	while (sk2 == NULL && i < j) {
    		if (idx == j) {
    			idx = 0;
    		}
    		sk2 = reuse->socks[idx];
    		i++;
    		idx++;
    	}
    
    out:
    	rcu_read_unlock();
    	return sk2;
    }
    

    上面的代码怎么跑起来就不多说了,简单讲就是用text_poke函数将原始函数的前面5个字节替换成jmp到hook函数的指令,具体参见:
    x86_64体系结构动态替换内核函数hotpatch之完结篇: https://blog.csdn.net/dog250/article/details/84572893

    为了避免在select的时候进行频繁的O(n)计算,需要在数据结构中添加字段以 记住 某些变量。所以说,需改源文件才是正道!

    由于select是数据通道的关键路径,绝不能执行耗时的O(n)操作,所以就把这些操作压缩到detach和add中进行,这也是我的下面这个版本和热补丁版本不同的地方。

    先看detach函数:

    void reuseport_detach_sock(struct sock *sk)
    {
        struct sock_reuseport *reuse;
        int i;
    
        spin_lock_bh(&reuseport_lock);
        reuse = rcu_dereference_protected(sk->sk_reuseport_cb,
                          lockdep_is_held(&reuseport_lock));
        rcu_assign_pointer(sk->sk_reuseport_cb, NULL);
        
    	// 首先把所有此socket占据的slot清空。
        for (i = 0; i < reuse->high_sock; i++) {
            if (reuse->socks[i] == sk) {
                reuse->socks[i] = NULL;
            }
        }
    
    	// 将所有在第一步清空的slot进行补充,即将其后面第一个不为NULL的socket补充到该slot
    	// 这个是一致性哈希的关键。
    	// 控制通道的O(n)并不是什么大问题,毕竟socket重启,断开,新建这种事不是什么频繁的操作。
        for (i = 0; i < reuse->high_sock; i++) {
            if (reuse->socks[i] == NULL) {
                int j = i + 1, k = 0;
                while (reuse->socks[j] == NULL && k++ < reuse->high_sock) {
                    j++;
                    if (j == reuse->high_sock)
                        j = 0;
                }
                reuse->socks[i] = reuse->socks[j];
            }
        }
        reuse->num_socks--;
        if (reuse->num_socks == 0)
            call_rcu(&reuse->rcu, reuseport_free_rcu);
        spin_unlock_bh(&reuseport_lock);
    }
    

    再看一下add操作:

    int reuseport_add_sock(struct sock *sk, struct sock *sk2)
    {
        struct sock_reuseport *reuse;
    
        if (!rcu_access_pointer(sk2->sk_reuseport_cb)) {
            int err = reuseport_alloc(sk2);
    
            if (err)
                return err;
        }
    
        spin_lock_bh(&reuseport_lock);
        reuse = rcu_dereference_protected(sk2->sk_reuseport_cb,
                          lockdep_is_held(&reuseport_lock)),
        WARN_ONCE(rcu_dereference_protected(sk->sk_reuseport_cb,
                            lockdep_is_held(&reuseport_lock)),
              "socket already in reuseport group");
    
        if (reuse->num_socks == reuse->max_socks) {
            reuse = reuseport_grow(reuse);
            if (!reuse) {
                spin_unlock_bh(&reuseport_lock);
                return -ENOMEM;
            }
        }
    
        // O(n)!!!
        if (reuse->socks[0] && reuse->socks[0] == reuse->socks[reuse->high_sock-1]) {
            reuse->socks[reuse->high_sock-1] = sk;
            goto setting;
        }
        
        // 将detach中的补充进行复位。不影响原始的socket。
        for (i = 1; i < reuse->max_socks; i++) {
        	// 旧socket复位
            if (reuse->socks[i] && reuse->socks[i] == reuse->socks[i-1]) {
                reuse->socks[i-1] = sk;
                break;
            }
            // 新socket添加,队尾pending。
            if (reuse->socks[i] == NULL) {
                reuse->socks[i] = sk;
                reuse->high_sock++;
                break;
            }
        }
    
    setting:
        /* paired with smp_rmb() in reuseport_select_sock() */
        smp_wmb();
        reuse->num_socks++;
        rcu_assign_pointer(sk->sk_reuseport_cb, reuse);
    
        spin_unlock_bh(&reuseport_lock);
    
        return 0;
    }
    

    最后我们看一下select操作,这是一个关键的操作,所以一定要简单,我已经将那些for循环等耗时的查找分担进detach和add里面了,所以留下了一个精简的select:

    struct sock *reuseport_select_sock(struct sock *sk,
                       u32 hash,
                       struct sk_buff *skb,
                       int hdr_len)
    {
        struct sock_reuseport *reuse;
        struct bpf_prog *prog;
        struct sock *sk2 = NULL;
        u16 socks;
        u16 high;
    
        rcu_read_lock();
        reuse = rcu_dereference(sk->sk_reuseport_cb);
    
        /* if memory allocation failed or add call is not yet complete */
        if (!reuse)
            goto out;
    
        prog = rcu_dereference(reuse->prog);
        socks = READ_ONCE(reuse->num_socks);
        // 除了使用high而不是num之外,select函数没有任何修改!
        high = READ_ONCE(reuse->high_sock);
        if (likely(socks)) {
            /* paired with smp_wmb() in reuseport_add_sock() */
            smp_rmb();
    
            if (prog && skb)
                sk2 = run_bpf(reuse, socks, prog, skb, hdr_len);
            else 
                sk2 = reuse->socks[reciprocal_scale(hash, high)];
        }
    
    out:
        rcu_read_unlock();
        return sk2;
    }
    

    几乎没有任何修改!

    注意,review代码,我们可以看到,high_sock字段是只增不减的,这个字段是一个取模的关键字段,它表示一个reuseport集群系统最大的socket数量。

    之所以将其设计为只增不减,是因为我没有办法区分一个尾部的slot中的socket释放,是有意的释放,还是故障导致的socket重启释放,所以也就只能先这么设计了。副作用就是, 你一定要让你的集群每一个socket全部都启动后,再开始提供服务!

    好了,现在看看效果!

    重复做上面的实验。从左到右依次是socket集群中的三个服务进程:
    在这里插入图片描述
    现在让我们重启第一个和第二个进程:
    在这里插入图片描述
    OK,就是这个效果。


    以上说的reuseport貌似都是在说UDP,事实上对于TCP来讲,很多事情是没有必要做的。因为TCP是连接保持的,只有在建立连接的那一刻需要reuseport来做负载均衡,此后在连接过程中,会有单独的socket来保持一个连接,而不像UDP那样每一个包都要过一遍reuseport。

    但是话也不能完全这么讲,TCP上层的连接语义可能并非基于TCP连接的,也就是说TCP也有可能不同的五元组连接对应一个会话。但那又如何呢?无非也还是一回事呗,在连接建立的时候把连接SYN数据报文hash到同一个socket上呗,但是且慢!有坑!

    UDP可以根据数据包的内容来做hash,而TCP呢?一个SYN包什么都没有携带,拿什么做hash呢?见招拆招的解法就是使用Fastopen机制了,唉,越扯越远了,且Fastopen也不是都支持的。

    总之,reuseport的一致性哈希之所以要 一致性 ,是因为下面的原因:

    • 如果服务端集群中的某个socket节点断开重启了,保证不影响其它socket节点上的服务。
    • 如果客户端断开重连了(五元组发生了变化),保证它连到服务器端集群中的同一个socket节点。

    其中第一点是本文描述的算法保证的,Linux内核本身迄至5.1版本并没有实现。第二点可以通过bpf机制注入一段代码来实现。


    OK,浙江温州皮鞋湿,下雨进水不会胖!

    展开全文
  • UVM——TLM通信机制(port、export、imp + analysis_port

    千次阅读 多人点赞 2020-04-26 00:40:41
      端口优先级:port > export > imp, 使用connect()建立连接关系时,只有优先级高的才能调用connect()做连接,即port可以连接port、export或者imp;export可以连接export或者imp;imp只能作为数据传送的重点,...

      TLM(transaction level modeling)是一个基于事务(transaction)的通信方式,通常在高抽象级的语言中被引用作为模块之间的通讯方式,例如SystemC或者UVM。TLM通信需要两个通信的对象,这两个对象分别称之为initiator和target。

    一、两对象与三端口

    1.1.两个通信对象:initiator、target

    • initiator:通信请求的发起方,属于initiator;
    • target : 通信请求的响应方,谁就属于target;

      注意:通信发起方并不代表了transaction的流向起点,即不一定数据是从initiator流向target,也可能是从target流向了initiator。因此,按照transaction的流向,我们又可以将两个对象分为producerconsumer。区分它们的方法:

    • producer:生产数据方,即为producer;
    • consumer:接收数据方,即为consumer;

    1.2.三个通信端口:port、export、imp——控制流的体现

    通信端口按照类型可以划分为三种:

    1. port通信请求方initiator的发起端,initiator凭借port端口才可以访问target。
    2. export:作为initiator和target中间层次的端口
    3. imp只能作为target接收请求的响应端,它无法作为中间层次的端口,所以imp的连接无法再次延伸。

      端口优先级:port > export > imp, 使用connect()建立连接关系时,只有优先级高的才能调用connect()做连接,即port可以连接port、export或者imp;export可以连接export或者imp;imp只能作为数据传送的重点,无法扩展连接。三种端口非是uvm_component的子类,应该使用new()函数在build_phase中创建。(注意:不能用create创建,端口不属于UVM树的一部分)。

    二、TLM通信方式——一对一传输

      按通信传输的方向可以分为单向(unidirection)和双向(bidirection)。
    需要说明的是,不论是单向传输还是双向传输都有阻塞和非阻塞之分。下面以阻塞传输为中心。

    • 单向传输:由initiator发起request transaction,数据有生产方producer发送到数据接收方consumer。
    • 双向传输:由initiator发起request transaction,传送至target;而target在消化了request transaction后,也会发起response transaction,继而返回给initiator。数据的流向是双向的。

    2.1.单向传输——put( )和get( )操作传输

    2.1.1. Put( )型操作传输

    • 单向通信中,port和export端口的参数只有一个;imp端口,参数有两个,第一个是传递的item类,第二个是实现该端口的component
    • 端口的实例化 使用new()函数在build_phase中创建
    • 阻塞的方法类型为task,这保证了可以实现等待事件和延时;非阻塞的方法类型为function,这确保了方法调用可以立刻返回。
    • 发起者的动作实现,最终会落到终点IMP所在的component中,因此必须 在imp所在的component中定义名字为put/get/transport的函数或任务,完成最终的数据传输操作。
      在这里插入图片描述
    class transaction extends uvm_transaction;      //传输数据包
       int id;
       int data;
       ...
    endclass
    
    class producer extends uvm_component;            //数据生产方producer
       ...
       uvm_blocking_put_port#(transaction)     put_port;     //定义端口
       function void build_phase(uvm_phase phase);
          put_port = new("put_port",this);               //创建实例化端口
       endfunction
       
       task run_phase(uvm_phase phase);        
          put_port.put(tr);        //1. 通过数据接收方consumer提供的任务put()处理数据,然后通过TLM传输数据过去
       endtask
    endclass
    
    class consumer extends uvm_component;            //数据接收方consumer
       ...
       uvm_blocking_put_imp#(transaction, consumer)     put_imp;     //定义端口
       function void build_phase(uvm_phase phase);
          put_imp = new("put_imp",this);               //创建实例化端口
       endfunction
       
       virtual task put(transaction tr);        //数据生产方producer会调用接收方consumer定义的put()任务
          process(tr);        //2. 通过数据接收方consumer提供的任务put()处理数据
       endtask
    endclass
    
    class environment extends uvm_env;       //环境层
       ...
       producer    p;    //数据生产方
       consumer    c;    //数据接收方
       ...
       virtual function void connect_phase(uvm_phase phase);
          p.put_port.connect(c.put_imp);       //建立连接
       endfunction
    endclass
    

    1)、上述代码分别在producer和consumer中声明并例化了两个端口实例:

         uvm_blocking_put_port #(transaction)         put_port;
         uvm_blocking_put_imp #(transaction,consumer)   put_imp;

    2)、在environment环境对producer与consumer进行连接之前,需要在consumer中实现两个端口对应的方法
        virtual task put( transaction  tr );    //阻塞端口需要定义为任务task
    3)、最后在environment中对两个组件之间的端口进行了连接,这使得producer的run phase中可以通过自身的两个端口间接调用consumer中的方法。需要注意的是,在调用方法之前的几个步骤是必不可少的:

         – 定义端口
         – 实现对应方法
         – 在上层将端口进行连接

    2.1.2.Get( )型操作传输
    在这里插入图片描述

    class transaction extends uvm_transaction;      //传输数据包
       int id;
       int data;
       ...
    endclass
    
    class producer extends uvm_component;            //数据生产方producer
       ...
       uvm_blocking_get_imp#(transaction,producer)     get_imp;     //定义端口
       function void build_phase(uvm_phase phase);
          get_imp = new("put_imp",this);               //创建实例化端口
       endfunction
        
       virtual task get(output transaction tr);        //在imp端口所在component中定义get()任务或者方法,output方向
          tr = transaction::type_id::create("tr",this);     
       endtask
    endclass
    
    class consumer extends uvm_component;            //数据接收方consumer
       ...
       uvm_blocking_get_port#(transaction, consumer)     get_port;     //定义端口
       function void build_phase(uvm_phase phase);
          put_imp = new("put_imp",this);               //创建实例化端口
       endfunction
    
       task run_phase(uvm_phase phase);        
          get_port.get(tr);        //通过imp所在component定义的get()处理数据,然后通过TLM传输数据
       endtask   
    endclass
    
    class environment extends uvm_env;       //环境层
       ...
       producer    p;    //数据生产方
       consumer    c;    //数据接收方
       ...
       virtual function void connect_phase(uvm_phase phase);
          c.get_port.connect(p.get_imp);       //建立连接
       endfunction
    endclass
    

    详细流程参考put( )型操作传输。

    2.2.双向传输——transport( )操作传输

      双向传输通信通过transport()任务实现。相当于一次put传输加一次get传输,可以在同一任务调用过程中完成REQ和RSP的发出和返回;
    在这里插入图片描述

    class comp1 extends uvm_component;
       `uvm_component_utils(comp1)
        uvm_blocking_transport_port #(itrans,otrans) t_port;
        ...
       virtual function void build_phase(uvm_phase phase);
          super.build_phase(phase);
          t_port=new("t_port",this);      //transort端口实例化
       endfunction
     
       virtual task run_phase(uvm_phase phase);
         super.run_phase(phase);
         itransactions itrs;        //生产数据,输入
         otransactions otrs;        //响应数据,返回
     
         itrs=new("itrs");
         t_port.transport(itr,otrs);       //调用comp2中声明的transport()
         ....
       endtask
    endclass
     
    class comp2 extends uvm_component;
       `uvm_component_utils(comp2)
        uvm_blocking_transport_imp #(itransactions,otransactions,comp2) t_imp;//注意imp端口多一个组件参数
        ...
        virtual function void build_phase(uvm_phase phase);
          super.build_phase(phase);
          t_imp=new("t_imp",this);        //例化端口
        endfunction
        task transport(input itransactions req, output otransactions rsp);      //编写transport方法
          rsp=new("rsp");
          .....
        endtask
    endclass
     
    class env1 extends uvm_env;
     `uvm_component_utils(env1)
     comp1 c1;
     comp2 c2;
      ...
     virtual function void connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      c1.t_port.connect(c2.t_imp);
     endfunction
    endclass
    

    1)、首先在comp1和comp2中分别例化了transport端口:

    • uvm_blocking_transport_port #(itrans,otrans)     t_port;
           //双向通信,port和export端口的参数有两个,分别表示REQ和RSP
    • uvm_blocking_transport_imp#(itrans,otrans,comp2)     t_imp;
           //双向通信,imp端口的参数有三个,分别表示REQ和RSP,以及实现该端口的component

    2)、然后在env1环境对comp1与comp2进行连接之前,需要在comp2中实现两个端口对应的方法 task transport();

    3)、最后在env1中对两个组件之间的端口进行了连接,这使得comp1的run_phase中可以通过自身的端口间接调用comp2中的方法;

    从上面的例码可以看出,类似于单向端口连接的是端口的例化和连接,不同的只是要求实现对应的双向传输任务。

      task transport(input itrans req, output otrans rsp);
    

    2.3.跨层次传输——uvm_analysis_port

    方法1:

      在复杂的UVM TB中常用到port跨多级component连接的情形,在多层次连接中,connect要按控制流逐层进行连接。在一个path中,始终只会有一个imp作为连接的结尾。

      以agent中的monitor与scoreboard通信为例:
    在这里插入图片描述

    class monitor extends uvm_monitor;
       ...
       uvm_analysis_port#(transaction)    analysis_port;     //声明端口
       virtual function void build_phase(uvm_phase phase);
          this.analysis_port=new("analysis_port",this);    //端口实例化
       endfunction
    endclass
    
    class agent extends uvm_agent;
       ...
       monitor   mon;
       uvm_analysis_port#(transaction)    analysis_port;    //声明端口
       virtual function void build_phase(uvm_phase phase);
          this.analysis_port=new("analysis_port",this);    //端口实例化
       endfunction
    
       virtual function void connect_phase(uvm_phase phase);
          mon.analysis_port.connect(this.analysis_port);     //相同端口的跨层次连接
       endfucntion
    endclass
    
    class environment extends uvm_env;
       ...
       agent  agt;
       scoreboard  sb;
       ...
       virtual function void connect_phase(uvm_phase phase);
          agt.analysis_port.connect(sb.analysis_imp);     //不同端口类型连接
       endfucntion
    endclass     
    

    方法2:(常用方法) 在agent中声明一个ap,但是不例化它,让其指向monitor中的ap。在env中可以直接连接agent的ap到scoreboard的imp

    class monitor extends uvm_monitor;
       ...
       uvm_analysis_port#(transaction)    ap;     //声明端口
       
       virtual function void build_phase(uvm_phase phase);
          this.ap=new("ap",this);               //端口实例化
       endfunction
       
       task main_phase(uvm_phase phase);
         super.mian_phase(phase);
         transaction  tr;
         ...
         ap.write(tr);
         ...
       endtask
    endclass
    
    class agent extends uvm_agent;
       ...
       monitor   mon;
       uvm_analysis_port#(transaction)    ap;    //声明端口
       ...
       virtual function void connect_phase(uvm_phase phase);
          ap=mon.ap;                            //1. 调用实例化的mon.ap赋值给为实例化的ap,(相当于连接并实例化)
          ...
       endfucntion
    endclass
    
    class scoreboard extends uvm_scoreboard;
      ...
      uvm_analysis_imp #(transaction, scoreboard)    analysis_imp;
      ...
      task write(transaction  tr);
        //do something on tr
      endtask
    endclass
    
    class environment extends uvm_env;
       ...
       agent  agt;
       scoreboard  sb;
       ...
       virtual function void connect_phase(uvm_phase phase);
          agt.ap.connect(sb.analysis_imp);       //2. 不同端口类型连接
       endfucntion
    endclass     
    

    三、TLM通信方式——一对多传输

      前面通信有一个共同的地方即都是端对端的方式,同时在target一端需要实现传输方法,例如put()或者get()。这种方式在实际使用过程中也不免会给用户带来一些烦恼:

    • 如何可以不自己实现这些传输方法,同时可以享受到TLM的好处
    • 对于monitor、coverage collector等组件在传输数据时,会存在一端到多端的传输,如何解决这一问题。

    3.1.通信管道——TLM FIFO

      uvm_tlm_fifo类是一个新的组件,它继承于uvm_component,且预先内置了多个端口、实现了多个对应方法供用户使用;功能类似mailbox #(T),该邮箱没有尺寸限制,用来存储数据类型T,而uvm_tlm_fifo的多个端口对应的方法都是利用该邮箱实现了数据读写。uvm_tlm_fifo所带的端口类型都是imp!!!

      在前面的例子中,我们通过port和imp实现了将数据从A发送到B,也可以在A和B之间加入一个FIFO进行缓存,如下图:
    在这里插入图片描述

    class my_env extends uvm_env;
       `uvm_component_utils (my_env)
       componentA compA;
       componentB compB;
     
       uvm_tlm_fifo #(transaction)    tlm_fifo;//定义TLM FIFO
       ... 
       virtual function void build_phase (uvm_phase phase);
          super.build_phase (phase);
          compA = componentA::type_id::create ("compA", this);
          compB = componentB::type_id::create ("compB", this);
     
          tlm_fifo = new ("tlm_fifo", this, 2);    //创建一个深度为2的FIFO,2省略,FIFO无限深
       endfunction
     
       virtual function void connect_phase (uvm_phase phase);
          compA.put_port.connect (tlm_fifo.put_export);
          compB.get_port.connect (tlm_fifo.get_export);
       endfunction
     
       virtual task run_phase (uvm_phase phase);
          forever begin
             #10 if (tlm_fifo.is_full ())//uvm_tlm_fifo常用的方法见UVM手册,FIFO为满时打印信息
                   `uvm_info ("UVM_TLM_FIFO", "Fifo is now FULL !", UVM_MEDIUM)
          end
       endtask
    endclass
    

      FIFO类型有两种,一种为上面介绍的uvm_tlm_fifo,另一种较常用的uvm_tlm_analysis_fifo。二者的唯一差别在于后者有一个analysis_port端口,且有一个write()函数,而前者没有。
      对于一般用户而言,只需要知道uvm_tlm_analysis_fifo的两个端口即可,即analysis_port与blocking_get_port,前者常用于fifo进数据,后者用于fifo出数据

    3.2.分析端口——Analysis port(一对多传输、最常用)

      UVM提供了一种分析端口(analysis port),它在组件中是以广播(broadcast)的形式向外发送数据的,而不管存在几个imp或者没有imp。分析端口的根据端口类型的不同分为:

    • uvm_analysis_port、 uvm_analysis_export、 uvm_analysis_imp
    • 只有一个操作:write()
    class componentB extends uvm_component;
       ...
       uvm_analysis_port #(transaction) ap;        //1.定义分析port端口
       ...
       virtual function void build_phase (uvm_phase phase);
          super.build_phase (phase);
          ap = new ("analysis_port", this);        //2.创建端口对象
       endfunction
     
      virtual task run_phase (uvm_phase phase);
        super.run_phase (phase); 
        for (int i = 0; i < 5; i++) begin
            simple_packet pkt = simple_packet::type_id::create ("pkt");
            pkt.randomize();
              ap.write (tr);                     //3.调用订阅端(subscriber)的write()方法,将数据发送出去
        end
      endtask
    endclass
    
    class subscriber extends uvm_component;
       ... 
       uvm_analysis_imp #(transaction, subscriber) analysis_export;//定义分析imp端口
       ...
       virtual function void write (transaction  tr);     //4. 定义write()方法
          `uvm_info (get_full_name(), "Sub got transaction", UVM_MEDIUM)
       endfunction
    endclass
    

    在这里插入图片描述
    在environment中通过分析端口将B与sub1、sub2、sub3连接,如下:

    class my_env extends uvm_env;
       `uvm_component_utils (my_env)
     
       componentA  compA;
       componentB  compB;
       sub         sub1; 
       sub         sub2; 
       sub         sub3; 
     
       function new (string name = "my_env", uvm_component parent = null);
          super.new (name, parent);
       endfunction
     
       virtual function void build_phase (uvm_phase phase);
          super.build_phase (phase);
          // Create an object of both components
          compA = componentA::type_id::create ("compA", this);
          compB = componentB::type_id::create ("compB", this);
          sub1 = sub::type_id::create ("sub1", this);
          sub2 = sub::type_id::create ("sub2", this);
          sub3 = sub::type_id::create ("sub3", this);
       endfunction
     
       virtual function void connect_phase (uvm_phase phase);
          compA.put_port.connect (compB.put_export);  
     
          compB.ap.connect (sub1.analysis_export);       //5.端口连接
          compB.ap.connect (sub2.analysis_export);
          compB.ap.connect (sub3.analysis_export);
       endfunction
    endclass
    

    1). 首先在componentB中定义分析port端口,并在run_phase阶段调用write()函数

    • uvm_analysis_port #(transaction)   ap;
    • ap.write (tr);

    2)、 在分析端口的订阅端(subscriber) ,我们需要定义write()方法

    3)、在environment中通过分析端口将B与sub1、sub2、sub3连接 ,如下:

    • compB.ap.connect (sub1.analysis_export);
    • compB.ap.connect (sub2.analysis_export);
    • compB.ap.connect (sub3.analysis_export);

    参考:https://blog.csdn.net/bleauchat/article/details/90757417

    展开全文
  • 通过PID获取Port,通过Port获取PID

    热门讨论 2012-04-18 14:41:30
    通过PID获取Port,通过Port获取PID 可参考此文章:http://blog.csdn.net/change518/article/details/7473475
  • WIN10平台下Prolific USB-to-Serial Comm Port线的驱动程序及安装方法。
  • usb_serial_port driver

    2013-10-24 11:15:04
    USB转串口驱动(usb_serial_port driver) USB 2.0 TO RS232 Cable USB 2.0 TO RS232 Converter USB TO RS232 Cable USB TO RS232 Converter
  • K8s中nodePortport、targetPort、hostPort介绍 1、nodePort 外部流量访问k8s集群中service入口的一种方式(另一种方式是LoadBalancer),即nodeIP:nodePort是提供给外部流量访问k8s集群中service的入口。比如外部...

    K8s中nodePort、port、targetPort、hostPort介绍

    1、nodePort

    外部流量访问k8s集群中service入口的一种方式(另一种方式是LoadBalancer),即nodeIP:nodePort是提供给外部流量访问k8s集群中service的入口。比如外部用户要访问k8s集群中的一个Web应用,那么我们可以配置对应service的type=NodePort,nodePort=30001。其他用户就可以通过浏览器http://node:30001访问到该web服务。而数据库等服务可能不需要被外界访问,只需被内部服务访问即可,那么我们就不必设置service的NodePort。

    作者:taj3991
    链接:https://www.jianshu.com/p/8275f2031c83
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    2、port

    k8s集群内部服务之间访问service的入口。即clusterIP:port是service暴露在clusterIP上的端口。mysql容器暴露了3306端口,集群内其他容器通过33306端口访问mysql服务,但是外部流量不能访问mysql服务,因为mysql服务没有配置NodePort。对应的service.yaml如下:
    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-service
    spec:
    ports:

    • port: 33306
      targetPort: 3306
      selector:
      name: mysql-pod

    3、targetPort

    容器的端口(最终的流量端口)。targetPort是pod上的端口,从port和nodePort上来的流量,经过kube-proxy流入到后端pod的targetPort上,最后进入容器。
    制作容器时暴露的端口一致(使用DockerFile中的EXPOSE),例如官方的nginx(参考DockerFile)暴露80端口。 对应的service.yaml如下

    apiVersion: v1
    kind: Service
    metadata:
    name: nginx-service
    spec:
    type: NodePort // 有配置NodePort,外部流量可访问k8s中的服务
    ports:

    • port: 30080 // 服务访问端口
      targetPort: 80 // 容器端口
      nodePort: 30001 // NodePort
      selector:
      name: nginx-pod

    4、hostPort

    这是一种直接定义Pod网络的方式。hostPort是直接将容器的端口与所调度的节点上的端口路由,这样用户就可以通过宿主机的IP加上来访问Pod了,如:

    apiVersion: v1
    kind: Pod
    metadata:
    name: influxdb
    spec:
    containers:
    - name: influxdb
    image: influxdb
    ports:
    - containerPort: 8086
    hostPort: 8086
    这样做有个缺点,因为Pod重新调度的时候该Pod被调度到的宿主机可能会变动,这样就变化了,用户必须自己维护一个Pod与所在宿主机的对应关系。
    使用了 hostPort 的容器只能调度到端口不冲突的 Node 上,除非有必要(比如运行一些系统级的 daemon 服务),不建议使用端口映射功能。如果需要对外暴露服务,建议使用 NodePort Service。

    5、总结

     总的来说,port和nodePort都是service的端口,前者暴露给k8s集群内部服务访问,后者暴露给k8s集群外部流量访问。从上两个端口过来的数据都需要经过反向代理kube-proxy,流入后端pod的targetPort上,最后到达pod内的容器。
     nodeport与hostport都是通过,主机ip+端口的方式访问,区别为:hostport确认的是固定主机ip,nodeport可以动态一些。
    
    展开全文
  • 关于Linux UDP/TCP reuseport 二三事

    万次阅读 2018-05-26 08:19:23
    聊到reuseport,大致要从四年前说起。 OpenVPN-以往的故事 当时要优化OpenVPN的并发性能,了解到socket有一个reuseport特性,于是非常兴奋,本着拿来主义的想法,无非就是在OpenVPN的源码里加一个setsockopt吧…...

    聊到reuseport,大致要从四年前说起。

    OpenVPN-以往的故事

    当时要优化OpenVPN的并发性能,了解到socket有一个reuseport特性,于是非常兴奋,本着拿来主义的想法,无非就是在OpenVPN的源码里加一个setsockopt吧…我们当时基于Linux 2.6.32开发,无奈并没有实现这个特性,后来的版本也只是实现了reuseport热备份模式,并没有实现负载均衡模式,也是无奈…

    不过无所谓,好在我对Linux网络部分的代码是很熟悉的,当我知道Linux 3.9内核实现了完备的reuseport负载均衡模式时,就迫不及待地将它移植到了2.6.32内核,也就半个小时不到的样子搞定,然后就可以愉快地玩OpenVPN了!

    故事零零散散地被记录,从未被整理,找到下面几篇,可见一斑:
    基于UDP服务的负载均衡方法https://blog.csdn.net/dog250/article/details/17061277
    OpenVPN多处理之-为什么一定要做https://blog.csdn.net/dog250/article/details/38154707
    Linux 4.6内核对TCP REUSEPORT的优化https://blog.csdn.net/dog250/article/details/51510823

    我玩reuseport的几个阶段-故事的延续

    在继续reuseport的故事之前,先来回顾一下reuseport的版本。

    围绕着reuseport如何来hash的细节,大致可以分为以下几个阶段:

    • 版本1:3.96内核轻移植到2.6.32内核的reuseport版本
      这个不多说,我当时只是保证能用即OK,二十分钟的移植工作当然是没有时间去深究的。
    • 版本2:加入OpenVPN session ID的版本
      SID版OpenVPN,即2013年底对OpenVPN的一个增强版本,我觉得我这个是个创举,一举解决了移动终端频繁的3G/Wifi,强信号/弱信号切换导致的断线重连问题。该版本的OpenVPN对应的Linux内核需要对reuseport逻辑进行修正,即用OpenVPN协议头中的sid来计算hash key,而不再使用传统的四元组。详情参见:
      OpenVPN移动性改造-靠新的session iD而不是IP/Port识别客户端https://blog.csdn.net/dog250/article/details/29180765
    • 版本3:3.96内核-4.5内核版本的数值分析伪随机版本
      返璞归真,自3.96原生内核,我便不再自己玩了,这个时候我已经不再继续做OpenVPN了,扔下一个烂摊子,发现reuseport已经被我玩坏了。
      于是仔细分析了原生的实现,其中的核心是next_pseudo_random32函数,这是一个数值分析方法里导出的公式,该版本的reuseport就是用它来做next伪随机数的,我们信赖这个公式,所以我们信赖最终的结果是均匀的。
    • 版本4:4.6内核的取模版本,顺带支持bpf
      这个版本就是迄至4.14版本中一直在用的,详情参见:
      Linux 4.6内核对TCP REUSEPORT的优化https://blog.csdn.net/dog250/article/details/51510823

    以上,除了版本2是我自己派生的之外,其它3个版本几乎都是原生的,不管是采用数值复分析的方法还是采用取模的方法,其最终的hash结果都是不可靠的。这种不可靠主要由两个原因导致,我来分别说。

    1. 数据接收端的socket变更导致的不一致

      以取模版的hash为例,考虑以下情景,一共建立了 n n 个reuseport的socket,那么到达数据包的四元组经过计算后对n取模得到 k1 k 1 ,此时有一个socket挂掉了,那么同样的四元组经过计算后将对 n1 n − 1 取模,得到 k2 k 2 ,很显然 k1 k 1 k2 k 2 是不相等的。如果socket和处理进程或者处理线程是绑定的话,那么单独进程/线程的新建,销毁将会导致hash结果的不一致。为此我们需要一种一致性hash的解法。

      注意,上述问题对于UDP而言,比TCP更加严重。对于TCP而言,hash的不一致仅仅影响新建连接三次握手的瞬间,而对于UDP,将会影响整个四元组的整个生命周期!

    2. 数据发送端的IP/Port变更导致的不一致

      对于UDP协议而言,我们不希望由于客户端切换了一个IP地址而导致整个应用层数据传输的中断,毕竟UDP仅仅只是起到运输的作用,它不像TCP那样和应用进程是强关联的。这种切换IP和端口的场景在移动设备上特别常见,就我个人而言,当我发现Wifi信号差的时候,就会直接禁掉Wifi,如此这般频繁切换,这个时候如果后台有数据正在传输,我当然不希望它由于我的这次切换而中断。
      那显然不能再用IP和Port来做hash计算源了。

    我在版本2中解决了以上的问题,所以我觉得这是个创举。版本2基本上就做了两件事:

    • 用OpenVPN协议头里新增加的SID来做hash源
    • 在服务器端实现一致性hash替代内核原生的数值分析hash

    效果如何呢?就说一件事,设备在2014年底就开始陆续上线了,可能现在还在运行也说不准…

    如今-故事依然在继续

    哈哈,近日里又碰到了这种事情,reuseport,reuseport,reuseport。已经三年没玩了,依然是觉得有趣。

    又有什么新玩法呢?玩法就不说了,这里指出一个问题。我们来看看最新内核的reuseport socket的hash定位方法:

    struct sock *reuseport_select_sock(struct sock *sk,
                       u32 hash,
                       struct sk_buff *skb,
                       int hdr_len)
    {
        struct sock_reuseport *reuse;
        struct bpf_prog *prog;
        struct sock *sk2 = NULL;
        u16 socks;
    
        rcu_read_lock();
        reuse = rcu_dereference(sk->sk_reuseport_cb);
    
        /* if memory allocation failed or add call is not yet complete */
        if (!reuse)
            goto out;
    
        prog = rcu_dereference(reuse->prog);
        socks = READ_ONCE(reuse->num_socks);
        if (likely(socks)) {
            smp_rmb();
    
            if (prog && skb) // 给了用户一个bpf接口可外部控制
                sk2 = run_bpf(reuse, socks, prog, skb, hdr_len);
            else // 取模
                sk2 = reuse->socks[reciprocal_scale(hash, socks)];
        }
    
    out:
        rcu_read_unlock();
        return sk2;
    }

    正如你所见,即便是如此现代化的实现,其hash结果依然是不一致的,上述代码中,如果reuse->num_socks发生了变化,那么同样的hash值将会被定向到不同的socket上,这显然不是我们想要的。

    然而,正如大家都知道的,Linux内核作为通用内核是不会管这种家务事的,那么事情显然需要进程自己来负责,于是这个bpf接口就显得很有必要,灌入一段选择socket的bpf代码即可。

    但是仔细观察bpf程序的执行返回值:

    static struct sock *run_bpf(struct sock_reuseport *reuse, u16 socks,
                    struct bpf_prog *prog, struct sk_buff *skb,
                    int hdr_len)
    {
        struct sk_buff *nskb = NULL;
        u32 index;
    
        ...
        if (!pskb_pull(skb, hdr_len)) {
            kfree_skb(nskb);
            return NULL;
        }
        // 注意,返回值只是一个index!!!
        index = bpf_prog_run_save_cb(prog, skb);
        ...
        // 依然是用index取socket
        return reuse->socks[index];
    }

    我们知道bpf代码是用户灌入的,而socket的index是内核维护的,如何建立二者的关联就是一个问题,显然内核并没有提供什么方便的接口让编程者可以方便快捷地建立二者之间的关联。

    那么我们自己加一个:

    // get the index of a socket index from a reuseport set
    #define SO_GETREUSEINX  XXX

    然后在sock_getsockopt里加入下面的代码即可:

        case SO_GETREUSEINX:
            {
                struct sock_reuseport *reuse;
                int i;
    
                spin_lock_bh(&reuseport_lock);
                reuse = rcu_dereference_protected(sk->sk_reuseport_cb, lockdep_is_held(&reuseport_lock));
                rcu_assign_pointer(sk->sk_reuseport_cb, NULL);
    
                for (i = 0; i < reuse->num_socks; i++) {
                    if (reuse->socks[i] == sk) {
                        v.val = i;
                        break;
                    }
                }
                spin_unlock_bh(&reuseport_lock);
            }

    有了这么一个小小的改动,使用bpf就可以非常随意了。我们可以在用户态集群的共享内存里维护一张映射表,index在集群建立之初通过调用每一个reuseport socket的SO_GETREUSEINX这个option来获取并初始化,hash算法可以简单地取模即可,按照内核中的算法来,如下所示:

    hash值index(索引,bpf的返回值)
    h0 h 0 0
    h1 h 1 1
    h2 h 2 2

    然后编写bpf程序,从hash值里取index。一切顺利,OK。此时,一旦有socket新增了或者退出了,用户态管理进程便需要重新生成这个映射表并且生成bpd注入到内核,更新既有的bpf程序代码。

    不多说,都明白。

    现在说一下如果不想用bpf程序怎么办?如何实现一个最简单的一致性hash呢?其实也不难,我们看一下当一个socket新增和退出时问题出在哪里,然后见招拆招地去解决它就好了。先看代码:

    void reuseport_detach_sock(struct sock *sk)
    {
        ...
        for (i = 0; i < reuse->num_socks; i++) {
            if (reuse->socks[i] == sk) {
                // 用最后一个socket补充到这个空位
                reuse->socks[i] = reuse->socks[reuse->num_socks - 1];
                // 递减总量,这是造成取模不一致的根源!!
                reuse->num_socks--;
                ...
                break;
            }
        }
        ...
    }

    问题似乎很好解决,就是说把下面两行代码改掉即可:

    reuse->socks[i] = reuse->socks[reuse->num_socks - 1];
    reuse->num_socks--;

    改成什么呢?按照一致性hash标准的接管方式来,一旦索引为 i i 的socket退出了,那么由它紧邻的那个socket暂时接管它的位置,同时记录i值,以备新建的补充socket填位:

    reuse->socks[i] = reuse->socks[i+1];
    //reuse->num_socks--;
    reuse->next_slot = i;

    此时如果有新的进程建立了一个新的reuseport socket,那么就补充到next_slot这个位置即可:

    reuse->socks[reuse->next_slot] = sk;

    是不是超级简单?

    细心的话你应该能看到一个问题,新增怎么办?,socket挂掉不递减num_socks是可行,那么新增socket不递增可以吗?显然不可以,那么怎么办?非常好办!请注意,我这里实现的是最简版本,而不是最优版本,先用起来再说呗。

    解决方案就是,如果你的集群中目前有 n n 个服务器,未来可能会增加,但上限就是2×n,我们不妨直接把num_socks初始化成 2×n 2 × n ,然后每隔2个slot填充相同的socket即可,如果有新的socket创建,就随机拆分,即reuse->next_slot以2为单位进行随机,然后随机到那个slot,就按照伙伴系统的算法进行拆分…嗯,棒极了。

    显然,不管再怎么好,这也不是内核的标准功能,也许,很多人正希望使用原生的实现呢…于是,一个新的sockopt是必要的,即SO_REUSEPORTHOLD。

    reuseport版本5

    于是出现了reuseport版本5,正如上节所述。

    展望-未来会发生什么

    reuseport一直以来都是青睐UDP而不是TCP!

    随着http2的逐渐进入视野,几乎全部基于tcp的http1正在加速退出,未来的UDP将大展宏图。

    最为先行者,Google已经将QUIC用在了其自家的Chrome浏览器上封装其自家的http流量,这开了一个大口子,就像G家的bbr开了个大口子一样。为什么是UDP?

    因为TCP过时了,过时了,过时了,因为TCP过时了!

    很多人都知道Google在TCP上的最新进展,那是因为教育的落后,我们付出了大量的时间精力学习复杂的一逼的TCP,却忽略了UDP。TCP是30年前的协议,它也是为30年前的网络场景准备的,然而我们像追星一样追了30年。其实Google的很多TCP优化都是从其在QUIC的试验田里移植过来的,Google可能早就想拥抱UDP了,其对TCP的态度更多的是兼容,而不是优化!TCP到底怎么了?

    • TCP强行打包了握手,按序…强买强卖
    • TCP实现可插拔功能非常不灵活
    • TCP为了节省空间开销付出了时间的代价(30年前的场景)
    • TCP完全基于ACK而全无NAK
    • TCP不得不在信息量极少的情况下实现失败的拥塞控制
    • QUIC的bbr性能比TCP的bbr性能高
    • TCP的脑残粉太多太多了

    UDP的优势在于灵活,比如它可以实现松散按序传输,特别是对于音视频类的传输非常棒,偶尔的丢包不会阻碍窗口向前滑行,拥塞控制可以基于ACK+NAK从而精确判断到底发生了什么…

    UDP的空间非常大,因为很多TCP的特性都要你自己实现,很多TCP中你不需要的东西你也可以随便抛弃,reuseport只是其中一个可以利用的,但绝不是唯一的一个,相信今后会有更多的基于UDP的可玩且好玩的东西出来。

    后记

    本来打算去大鹏杨梅坑过个完整的周末,无奈周六上午小小有家长会,只能下午出发了…别说搞IT的加班上瘾,小学老师也是一个加班上瘾的群体…多少个周末都被小小小学的各种活动所破坏,都上一周班了,就不能消停点吗?!

    展开全文
  • Configure Virtual Serial Port Driver破解版

    热门讨论 2012-10-25 13:52:40
    Configure Virtual Serial Port Driver,能在PC上模拟两个虚拟串口,一个发送一个接受,可以看自己串口发送的内容。
  • K8s nodePortport、targetPort、hostPort

    千次阅读 2020-03-16 16:22:17
    1. nodePort 外部流量访问k8s集群中service入口的一种方式(另一种方式是LoadBalancer),即nodeIP:nodePort是提供给外部流量访问k8s集群中service的入口。比如外部用户要访问k8s集群中的一个Web应用,那么我们可以...
  • nginx源码分析—reuseport的使用

    千次阅读 2019-06-13 21:08:35
    本文主要介绍nginx中reuseport的使用,文中代码较多,阅读本文需要读者对nginx的事件模块以及listen配置过程有了解。 由于nginx比较复杂,且作者对nginx的理解有限,文章难免存在疏忽之处,敬请指出! 一、reuseport...
  • Port-Channel 是配置层面上的一个物理端口组,配置到port group里面的物理端口才可以参加链路汇聚,并成为port channel里的某个成员端口。在逻辑上,port group 并不是一个端口,而是一个端口序列。加入port group ...
  • nodePort提供了集群外部客户端访问service的一种方式,:nodePort提供了集群外部客户端访问service的端口,即nodeIP:nodePort提供了外部流量访问k8s集群中service的入口。 比如外部用户要访问k8s集群中的一个Web应用...
  • nodePort 提供了集群外部客户端访问 Service 的一种方式,nodePort 提供了集群外部客户端访问 Service 的端口,通过 nodeIP:nodePort 提供了外部流量访问k8s集群中service的入口。 比如外部用户要访问k8s集群中的一...
  • port的eb mcal配置方法
  • DisplayPort

    千次阅读 2018-12-19 10:32:33
    https://en.wikipedia.org/wiki/DisplayPort#Specifications From Wikipedia, the free ...DisplayPort (DP) is a digital display interface developed by a consortium of PC and chip manufacturers an...
  • K8s的Service发布主要有3种type,type=ClusterIP,表示仅内部可访问服务,type=NodePort,表示通过NodePort对外暴露服务,type=LoadBalancer,表示通过LoadBalancer对外暴露服务(底层对接NodePort,一般公有云才支持...
  • nginx启用reuseport

    千次阅读 2020-05-15 09:55:43
    测试结果发现服务器nginx的cpu利用率恨不均匀,后来查到有reuseport这个参数,放在listen后面 listen 80 default_server reuseport; 增了了试验了下,果然好用。 下图是对比,上面是不开启reuseport,可以看到cpu...
  • Virtual Serial Port Driver 7.1.289 注册机和汉化文件
  • Kubernetes 调整 nodePort 端口范围

    千次阅读 2019-08-15 21:19:15
    默认情况下,k8s 集群 nodePort 分配的端口范围为:30000-32767,如果我们指定的端口不在这个范围就会报类似下面这样的错误: Error: release kong failed: Service “kong-kong-admin” is invalid: spec.ports[0]...
  • Neutron中Portbinding扩展模块详解

    千次阅读 2018-11-02 00:24:16
    Extension以及Port binding简述 Port binding 的属性 Port binding 的属性在Neutron的mechanism driver中作用 SimpleAgentMechanismDriverBase SriovNicSwitchMechanismDriver LinuxbridgeMechanismDriver ...
  • usb over network 注册机,Virtual Serial Port Kit 注册机,Network Serial Port Kit注册机,Serial Port Redirector注册机,Serial Port Splitter,Serial Port Mapper,Access Port 串口调试工具等fabulatech产品...
  • SO_REUSEADDR和SO_REUSEPORT选项

    千次阅读 2018-06-29 12:17:58
    其中对socket选项SO_REUSEADDR和SO_REUSEPORT写了一些demo,文章根据测试结果对SO_REUSEADDR选项和SO_REUSEPORT选项做一个总结,同时对博客的总结做一个纠正。  先来了解一下socket默认的行为:  ·每个TCP连...
  • displayport1.4

    千次阅读 2019-12-10 10:27:07
    DisplayPort1.4协议学习DP的背景什么是displayport?已经有HDMI为什么还要开发DP?HDMI 与 DP的主要区别? DP的背景 什么是displayport? DP是一种视频传输接口协议,是由视频电子标准协会(VESA)开发标准化的数字式...
  • vspd虚拟串口最新版 安装好之后,用vspdctl.dll覆盖安装路径原文件即可破解
  • Linux 4.6内核对TCP REUSEPORT的优化

    万次阅读 2016-05-26 21:55:00
    下班的班车上,用手机那令人遗憾的屏幕目睹了Linux 4.6的一些新特性,让我感兴趣的有两点,第一是关于reuseport的,这也是本文要阐释的,另外一个是关于KCM(Kernel Connection Multiplexor)的,而这个是我本周末计划...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,001,138
精华内容 800,455
关键字:

port