深入理解Linux网络——内核与用户进程协作之同步阻塞方案(BIO)

news2024/11/26 16:57:01

文章目录

    • 一、相关实际问题
    • 二、socket的直接创建
    • 三、内核和用户进程协作之阻塞方式
      • 1)等待接收消息
      • 2)软中断模块
      • 3)同步队列阻塞总结

在上一部分中讲述了网络包是如何从网卡送到协议栈的(详见深入理解Linux网络——内核是如何接收到网络包的),接下来内核还有一项重要的工作,就是在协议栈接收处理完输入包后要通知到用户进程,如何用户进程接收到并处理这些数据。

进程与内核配合有多种方案,这里我们这分析两种典型的:

  1. 同步阻塞方案(Java中习惯叫BIO)

  2. 多路IO复用方案(Java中对应NIO)

    • Linux多路复用有select、poll、epoll,这里只讲性能最优秀的epoll

本文主要讲的是同步阻塞模式的实现方案,多路IO复用方案及问题解答见文章深入理解Linux内核网络——内核与用户进程协作之多路复用方案(epoll)

一、相关实际问题

  1. 阻塞到底是怎么一回事
  2. 同步阻塞IO都需要哪些开销
  3. 多路复用epoll为什么就能提高网络性能
  4. epoll也是阻塞的吗
  5. redis为什么网络性能突出

二、socket的直接创建

以开发者的角度来看,调用socket函数可以创建一个socket

int main()
{
    int sk = socket(AF_INET, SOCK_STREAM, 0);
    ......
}

等这个socket函数调用执行完以后,用户层面看到返回的是一个整数型的句柄,但其实内核在内部创建了一系列的socket相关的内核对象(不止一个)。它们之间相互的关系如下:

在这里插入图片描述

socket在内核中的定义如下:

struct socket {  
    socket_state            state;  
    unsigned long           flags;  
    const struct proto_ops *ops;  
    struct fasync_struct    *fasync_list;  
    struct file             *file;  
    struct sock             *sk;  
    wait_queue_head_t       wait;  
    short                   type;  
};

typedef enum {  
    SS_FREE = 0,            //该socket还未分配  
    SS_UNCONNECTED,         //未连向任何socket  
    SS_CONNECTING,          //正在连接过程中  
    SS_CONNECTED,           //已连向一个socket  
    SS_DISCONNECTING        //正在断开连接的过程中  
}socket_state;

socket是内核抽象出的一个通用结构体,主要是设置了一些跟fs相关的字段,而真正跟网络通信相关的字段结构体是struct sock。

struct sock是网络层对于struct socket的表示,其中成员非常多,这里只介绍其中一部分。

  1. sk_prot和sk_prot_creator,这两个成员指向特定的协议处理函数集,其类型是结构体struct proto,该结构体也是跟struct proto_ops相似的一组协议操作函数集。这两者之间的概念似乎有些混淆,可以这么理解,struct proto_ops的成员操作struct socket层次上的数据,处理完了,再由它们调用成员sk->sk_prot的函数,操作struct sock层次上的数据。即它们之间存在着层次上的差异。struct proto类型的变量在协议栈中总共也有三个,分别是mytcp_prot,myudp_prot,myraw_prot,对应TCP, UDP和RAW协议。

  2. sk_state表示socket当前的连接状态,是一个比struct socket的state更为精细的状态,其可能的取值如下:

    • enum {  
         TCP_ESTABLISHED = 1,  
         TCP_SYN_SENT,  
         TCP_SYN_RECV,  
         TCP_FIN_WAIT1,  
         TCP_FIN_WAIT2,  
         TCP_TIME_WAIT,  
         TCP_CLOSE,  
         TCP_CLOSE_WAIT,  
         TCP_LAST_ACK,  
         TCP_LISTEN,  
         TCP_CLOSING, 
        
         TCP_MAX_STATES; 
      }
      
    • 这些取值从名字上看,似乎只使用于TCP协议,但事实上,UDP和RAW也借用了其中一些值,在一个socket创建之初,其取值都是TCP_CLOSE,一个UDP socket connect完成后,将这个值改为TCP_ESTABLISHED,最后,关闭sockt前置回TCP_CLOSE,RAW也一样。
  3. sk_rcvbuf和sk_sndbuf:表示接收和发送缓冲区的大小。这两个值是动态的,应用程序可以通过setsockopt系统调用来改变它们的值。但是,这些值也受到了一些全局内核参数的限制(通常由/proc/sys/net/core/rmem_default(对于接收缓冲区)和/proc/sys/net/core/wmem_default(对于发送缓冲区)这两个内核参数来决定)。

  4. sk_receive_queue和sk_write_queue:接收缓冲队列和发送缓冲队列,队列里排列的是套接字缓冲区struct sk_buff,队列中的struct sk_buff的字节数总和不能超过缓冲区大小的设定。在sock实例创建的时候初始化的,最开始为空的队列(双向链表)。

  5. struct inet_sock:这是INET域专用的一个socket表示,它是在struct sock的基础上进行的扩展,在基本socket的属性已具备的基础上,struct inet_sock提供了INET域专有的一些属性,比如TTL,组播列表,IP地址,端口等,完整定义如下:

    • struct inet_sock {  
                  struct sock     sk;  
      #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)  
                  struct ipv6_pinfo   *pinet6;  
      #endif  
                  __u32           daddr;          //IPv4的目的地址。  
                  __u32           rcv_saddr;      //IPv4的本地接收地址。  
                  __u16           dport;          //目的端口。  
                  __u16           num;            //本地端口(主机字节序)。  
                  __u32           saddr;          //发送地址。  
                  __s16           uc_ttl;         //单播的ttl。  
                  __u16           cmsg_flags;  
                  struct ip_options   *opt;  
                  __u16           sport;          //源端口。  
                  __u16           id;             //单调递增的一个值,用于赋给iphdr的id域。  
                  __u8            tos;            //服务类型。  
                  __u8            mc_ttl;         //组播的ttl  
                  __u8            pmtudisc;  
                  __u8            recverr:1,  
                                  is_icsk:1,  
                                  freebind:1,  
                                  hdrincl:1,      //是否自己构建ip首部(用于raw协议)  
                                  mc_loop:1;      //组播是否发向回路。  
                  int             mc_index;       //组播使用的本地设备接口的索引。  
                  __u32           mc_addr;        //组播源地址。  
                  struct ip_mc_socklist   *mc_list;   //组播组列表。  
                  struct {  
                      unsigned int        flags;  
                      unsigned int        fragsize;  
                      struct ip_options   *opt;  
                      struct rtable       *rt;  
                      int                 length;  
                      u32                 addr;  
                      struct flowi        fl;  
                  } cork;  
              };
      

sock_create是创建socket的主要位置,其中sock_create又调用了__sock_create

int __sock_create(struct net *net, int family, ...)
{
    struct socket *sock;
    const struct net_proto_family *pf;
    ......

    // 分配socket对象
    sock = sock_alloc();
    // 获得每个协议族的操作表
    pf = rcu_dereference(net_families[family]
    // 调用指定协议族的创建函数,对于AF_INET对应的就是inet_creat
    err = pf->create(net, sock, protocol, kern);
}

在__sock_create里,首先调用sock_alloc来分配一个struct socket的内核对象,接着获取协议族的操作函数表调用其create方法,对于AF_INET协议族来说,执行的是inet_create方法。

static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
    struct sock *sk;

    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
	// 将inet_stream_ops赋值到socket->ops上
	sock->ops = answer->ops;
	// 获得tcp_prot
	answer_prot = answer->prot;
	// 分配sock对象,并把tcp_prot赋值到sk->prot上
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
	// 对sock对象进行初始化
	sock_init_data(sock, sk);
    }
}

static struct inet_protosw inetsw_array[] = 
{
    {
    .type = SOCK_STREAM;
    .protocol = IPPROTO_TCP,
    .prot = &tcp_prot,
    .ops = &inet_stream_ops,
    .no_check = 0,
    .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
    },
}

在inet_create中,根据类型SOCK_STREAM查找到对于TCP定义的操作方法实现集合inet_stream_ops和tcp_prot,并把它们发别设置到socket->ops和sk->prot上。

最后的sock_init_data将sk中的sk_data_ready函数指针进行了初始化(也包括设置其他函数指针),设置为默认的sock_def_readable,同时也会初始化sk_receive_queue和sk_write_queue为空队列

inetsw_array存储了AF_INET类型套接字的的所有网络协议

在这里插入图片描述

当软中断上收到数据包时会通过调用sk_data_ready函数指针(实际上被设置成了sock_def_readable)来唤醒sock上等待的进程

至此一个tcp对象,确切的说是AF_INET协议族下的SOCK_STREAM对象就算创建完成了,这里花费了一次socket系统调用的开销

三、内核和用户进程协作之阻塞方式

同步阻塞IO总体流程如下

在这里插入图片描述

1)等待接收消息

查看recv函数的底层实现。首先通过strace命令追踪,可以看到clib库函数recv会执行recvfrom系统调用。

进入系统调用后,用户进程就进入了内核态,执行一系列的内核协议层函数,然后到socket对象的接收队列中查看是否有数据,没有的话就把自己添加到socket对应的等待队列里然后让出CPU,操作系统选择下一个就绪状态的进程来执行。

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
		unsigned int, flags, struct sockaddr __user *, addr,
		int __user *, addr_len)
{
    struct socket *sock;
    // 根据传入的fd找到socket对象
    sock = sock_lookup_light(fd, &err, &fput_needed);
    ......
    err = sock_recvmsg(sock, &msg, size, flags);
    ......
}

后续的调用顺序为:

sock_recvmsg ==> __sock_recvmsg ==> __sock_recvmsg_nosec

在__sock_recvmsg_nosec中会去调用socket对象proto_ops里的recvmsg,在AF_INET中其指向的是inet_recvmsg方法。

而在inet_recvmg中,会去调用socket中的sock对象的sk->sk_prot->recvmsg,在SOCK_STREAM中它的实现是tcp_recvmsg方法。

int tcp_recvmsg(struct kiocb *iocb, strcut sock * sock, struct msghdr *msg, 
size_t len, int nonblock, int flags, int *addr_len)
{
    int copied = 0;
    ......
    do {
 	// 遍历接收队列接收数据
	skb_queue_walk(&sk->sk_receive_queue, skb) {
	    ......
	}
	......
    }
    if(copied >= target) {
 	release_sock(sk);
	lock_sock(sk);
    } else // 如果没有收到足够数据,启用sk_wait_data阻塞当前进程
	sk_wait_data(sk, &timeo);
}

可以看到这里会去遍历socket的接收队列,如果接收到的数据不满足目标数量(使用recv时会传入要接收的字节数)则会阻塞当前进程,具体阻塞方法的实现逻辑如下

int sk_wait_data(struct sock *sk, long *timeo)
{
    // 当前进程(current)关联到所定义的等待队列项上
    DEFINE_WAIT(wait);
    // 调用sk_sleep获取sock对象下的wait并准备挂起,将进程状态设置为可打断
    prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
    set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
    // 通过调用schedule_timeout让出CPU,如何进行睡眠
    rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue);
    ......
}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
#define DEFINE_WAIT_FUNC(name, function) wait_queue_t name = {           \
						.private = current	 \
						.func = function	 \
						.task_list = LIST_HEAD_INIT((name).task_list) }

首先在DEFINE_WAIT宏下**,定义了一个等待队列项wait**,在这个新的等待队列项上注册了回调函数autoremove_wake_function,并把当前进程描述符current关联到其.private成员上

task_list = LIST_HEAD_INIT((name).task_list)将wait_queue_t的task_list成员初始化为一个空的链表头。LIST_HEAD_INIT是一个宏,它接受一个list_head类型的变量,并将它初始化为一个空的链表头。在这个宏定义中,(name).task_list实际上就是新定义的wait_queue_t变量的task_list成员。
所以,这行代码的意思就是将新定义的wait_queue_t变量的task_list成员初始化为一个空的链表头。这是必要的步骤,因为在wait_queue_t被添加到等待队列之前,它的task_list必须被初始化为一个有效的链表节点。

prepare_to_wait()中会将wait变量的task_list成员添加到wait_queue_head_t类型的等待队列中。也就是说,task_list成员会被链接到sk_sleep()返回的等待队列中。

typedef struct __wait_queue_head wait_queue_head_t;

struct __wait_queue_head {
   spinlock_t lock;
   struct list_head task_list;
};

紧接着调用sk_sleep获取sock对象下的等待队列列表头wait_queue_head_t。

接着调用prepare_to_wait来把新定义的等待队列项wait插入sock对象的等待队列,这样后面当内核收完数据产生就绪事件的时候,就可以查找socket等待队列上的等待项,进而可以找到回调函数和等待该socket就绪时间的进程了

最后调用sk_wait_event让出CPU,进程将进入睡眠状态,这会导致一次进程上下文的开销,而这个开销是昂贵的,大约需要花费几个微秒的CPU时间

2)软中断模块

上篇文章中我们降到了网络包到网卡之后是怎么被网卡接收最后再交给软中断处理的,最后讲到了ip_rcv根据inet_protos和数据包的协议将包交给上层协议栈的处理函数。软中断(也就是ksoftirqd线程)收到数据包以后,发现是TCP包就会执行tcp_v4_rcv函数,这里直接从TCP协议的接收函数tcp_v4_rcv开始。

int tcp_v4_rcv(struct sk_buff *skb)
{
    ......
    th = tcp_hdr(skb); // 获取tcp header
    iph = ip_hdr(skb); // 获取ip header
    // 根据数据包header中的IP、端口信息查找对应的socket
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    ......
    // socket未被用户锁定
    if(!sock_owned_by_user(sk)) {
	{
	    if(!tcp(prequeue(sk, skb))
		ret = tcp_v4_do_rcv(sk, skb);
	}
    }
}

首先根据收到的网络包的header里的source和dest信息在本机上查询对应的socket

tcp_hashinfo是一个散列表,用于存储所有活动的TCP套接字,从中查找与这个数据包对应的sock(即struct sock实例)。如果找到了匹配的套接字,就说明有一个连接正在接收这个数据包的源IP和端口发送的数据。

找到以后,如果数据包没有被处理则进入接收的主体函数tcp_v4_do_rcv

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    if(sk->sk_state == TCP_ESTABLISHED) {
	// 执行链接状态下的数据处理
	if(tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
	    rsk = sk;
	    goto reset;
	}
	return 0;
    }

    // 其他非ESTABLISHED状态的数据包处理
    ......
}

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, unsigned int len)
{
    ......
    // 接收数据放到队列中
    eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen);
    // 数据准备好,唤醒socket上组色调的进程
    sk->sk_data_ready(sk, 0);
}

假设处理的是ESTABLISHED状态下的包(即已经完成握手,建立连接),这样就又进入了tcp_rcv_established函数进行处理。

在tcp_rcv_established中完成了将接收到的数据放到socket的接收队列尾部,并调用sk_data_ready来唤醒在socket上等待的用户进程(创建socket时在sock_init_data函数里将该指针设置成了sock_def_readable)。

**唤醒进程时,即使等待队列里有多个进程阻塞着,也只唤醒一个进程,避免惊群。**会从头部开始依次检查每一个进程,看看是否满足唤醒的条件。如果满足条件,就将该进程唤醒。

在等待队列中,进程是按照它们进入队列的顺序排列的,即先进入队列的进程在队列的前面,后进入队列的进程在队列的后面

在前面调用recvfrom时,当数据不够后调用的sk_wait_data函数中使用DEFINE_WAIT定义了等待队列项的细节,并且把curr->func设置成了autoremove_wake_function,那么在唤醒进程时会去调用这个函数,它会去调用default_wake_function将因为等待而被阻塞的进程唤醒。这个函数执行完之后,这个进程就可以就可以被推入可运行队列里,在这里又将产生一次进程上下文切换的开销

3)同步队列阻塞总结

同步阻塞方式接收网络包的整个过程分为两个部分:

  1. 我们自己的代码所在的进程:我们调用的socket()函数会进入内核态创建必要的内核对象。recv()函数会在进入内核态以后负责查看接收队列,以及在没有数据可以处理的时候把当前进程组色调,让出CPU。
  2. 硬中断、软中断上下文:在这些组件中,将包处理完后会放到socket的接收队列中,然后根据socket内核对象找到其等待队列中正在因为等待而被阻塞掉的进程,将它唤醒。

每次一个进程专门为了等待一个socket上的数据就被从CPU上拿出来,然后换上另一个进程。等到数据准备好,睡眠的进程又会被唤醒,总共产生两次进程上下文切换开销。根据业界的测试,每一次切换大约花费3-5微妙。

然而从开发者的角度而言,进程上下文切换其实没有做有意义的工作。如果是网络IO密集型的应用,CPU就会被迫不停地做进程切换这种无用功。

这种模式在客户端角色上现在还存在使用的情形,因为你的进程可能确实需要等MySQL的数据返回成功之后才能渲染页面返回给用户,否则什么也干不了。

而在服务端角色上,这种模式完全无法使用。因为这种模型里的socket和进程是一对一的,现在的单台机器要承载成千上万甚至更多的用户连接请求,如果用上面的方式,就得为每个用户请求都创建一个进程,否则无法同时处理多个用户的请求,然而这肯定是不现实的。

所以我们需要更高效的网络IO模型!可前往深入理解Linux内核网络——内核与用户进程协作之多路复用方案(epoll)继续学习多路IO复用解决方案~

参考资料

《深入理解Linux网络》—— 张彦飞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/746342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

postman测试接口出现404

postman测试接口出现404 1.用postman调试接口的过程中,出现404的情况,但是接口明明已调到了,而且数据也已经存入数据库了,这让我感到很疑惑。看网上的解决办法检查了我的路径,提交方式、参数类型等都是正确的&#xf…

Java反射(Reflection)总结

目录 Java反射概述 Class类 Class对象获取的三种方式: Class类的常用方法 动态加载机制: 通过反射调用构造方法 Constructor类 通过反射获取继承关系 通过反射访问字段(成员变量) 获取字段值: 设置字段值 通过反…

MySQL数据库(四)

前言 本文讲述索引和事务,以及JDBC。 是关于MySQL的最后一弹啦~~ 重点小知识:MySQL数据库的存储结构是B树!! 目录 前言 一、索引 (一)查看索引 (二)创建索引 (三&am…

【JVM】14. 堆外内存

文章目录 堆外内存的意义堆外内存(Off-heap memory)是指在计算机内存管理之外进行分配和使用的内存空间。与堆内内存(Heap memory)不同,堆外内存不受Java虚拟机(JVM)的垃圾回收机制控制,需要手动进行内存的分配和释放。 堆外内存通常由操作系统提供支持,可以通过直接…

3.利用matlab求平均值和中位数(matlab程序)

代码及运行结果 %% 算术平均值 clear all; A[1 2 3 4;4 4 4 4] m1mean(A) %对列元素求算术平均值 m2mean(A,2) %对行元素求算术平均值 %% 忽略非数计算算术平均值 clear all; A[1 4 nan 5;6 nan 7 nan] m1mean(A) %有nan就是nan…

深度学习-图像分类篇一:基础理论

开头言 学东西前总是爱问,这个学了有什么用,会用就行了么。能够回答你这个问题的人,都是学过的(只有学过才有资格告诉你有没有用),然而知识往往就是这样,学的人越多越没有用,我可以…

使用 Google 的 zx 库编写切换微信小程序环境的脚本

背景 微信小程序目前为止还没有提供API或者具体的配置方式,给我们设置环境变量,所以还得自己想办法。 创建一个 shell 脚本——一个由诸如 Bash 或 zsh 之类的 shell 执行的脚本——可以是自动化重复任务的好方法。Node.js 似乎是编写 shell 脚本的理想…

Apache Calcite 简介

这张图上列的,是直接使用 Apache Calcite 或者至少相关联的项目。大家肯定能在里面找到很多自己熟悉的项目。 那 Apache Calcite 究竟是干嘛的,又为什么能这么流行呢? 首先,摆一个应该没多少人会反对的共识:SQL 是编程领域最流行的语言。 有 MySQL、Oracle 之类使用 SQL…

Linux---函数库和Makefile

Linux---函数库和Makefile 函数库动态库静态库例 Linux项目自动化构建工具-make/Makefile背景实例代码原理项目清理 函数库 方法实现就是在库当中,库其实就是把源文件经过一定的翻译,然后打包,只给你提供一个文件计科,不用给你提…

Spring -- Bean的生命周期

BeanDefinition Spring容器在进行实例化时&#xff0c;会将xml配置的<bean>的信息封装成一个BeanDefinition对象&#xff0c;Spring根据BeanDefinition来创建Bean对象&#xff0c;里面有很多的属性用来描述Bean BeanDefinition 中几个重要方法如下 beanClassName&#…

阿里云——云服务器基础运维与管理

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 写在前面 学习目标&#xff1a; 一.3个理由拥抱云服务器 1.什么是云服务器 2.使用…

人物百科词条创建教程分享,人物创建百科有什么要求

人物百科词条的创建是一个相对复杂的过程&#xff0c;需要遵循一定的规范和要求。以下是一个人物百科词条创建教程的分享&#xff0c;包括创建要求、步骤和注意事项&#xff0c;接下来伯乐网络传媒就来给大家好好讲一讲。 一、人物百科词条创建要求 1. 真实性&#xff1a;创建…

ELK-日志服务【filebeat-安装使用】

目录 【1】安装Filebeat 【2】配置-测试 【3】配置使用Filebeat 【4】filebeat-收集系统文件日志 【5】配置filebeat&#xff0c;将/var/log/all.log日志采集到es集群中 【6】定制索引名称 【7】收集多个web节点的日志&#xff0c;输出到相同的索引中 【8】filebeat-收…

【并查集+组合计数】ABC226 E

E - Just one (atcoder.jp) 题意&#xff1a; 思路&#xff1a; 首先有个条件&#xff1a;每个点只有一个出边 如果没有直接看出结论&#xff0c;可以画几个链和树等特殊的图&#xff0c;可以发现&#xff0c;树是不满足条件的&#xff0c;环是满足条件的&#xff0c;因此可…

管理类联考——数学——记忆篇——数字编码

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;考取过HCIE Cloud Computing、CCIE Security、CISP、RHCE、CCNP RS、PEST 3等证书。&#x1f433; &#x1f495;兴趣爱好&#xff1a;b站天天刷&…

Javaweb开发环境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程

Javaweb开发环境Myeclipse6.5 JDK1.6 Tomcat6.0 SVN1.8配置教程 javaweb开发环境的配置也是比较繁琐的一件事情&#xff0c;虽然理论上使用记事本&#xff0c;完全可以写出一个Javaweb工程&#xff0c; 但是在团队大型开发的Javaweb过程中&#xff0c;你必须配置好Eclipse、SV…

【算法集训之线性表篇】Day 08

文章目录 题目基本设计思想思路一思路二 代码实现效果 题目 已知一个整数数列A{a0,a1,a2,…,an-1}&#xff0c;其中0<ai<n(0<i<n)。若存在ap1ap2ap3…apmx且m>n/2(0<pk<n,1<k<m)&#xff0c;则成x为A的主元素。例如A{0,5,5,3,5,7,5,5}&#xff0c;…

Python模拟MQTT v3.1.1服务器

示例代码 import logging import asyncio from hbmqtt.broker import Broker# 设置日志级别为DEBUG logging.basicConfig(levellogging.DEBUG)# 创建MQTT服务器 broker Broker()# 启动MQTT服务器 async def start_broker():await broker.start()# 停止MQTT服务器 async def s…

好用到哭!后悔没有早点看到这个微信多账号管理工具!

作为一款及时通讯软件&#xff0c;微信是许多企业进行客户运营的选择工具。企业的员工利用微信与客户进行沟通&#xff0c;并且许多客单也都在微信上如数完成。然而&#xff0c;微信本身并不具备运营需要的很多功能&#xff0c;所以需要借助第三方工具来实现。 安全吗&#xf…

day20 数组指针

int main(void) {char *name[5] {"hello","china","beijing","project","Competer"};int i;for(i0;i<5;i){printf("%s\n",name[i]);//结果打印五个字符串}return 0; }数组指针&#xff0c;存了5个指针&#x…