第11讲:BootService 核心实现解析,Agent 的“地基”原来是这样的

news2024/10/7 12:18:27

之前介绍了 ServiceManager 加载并初始化 BootService 实现的核心逻辑。下图展示了 BootService 接口的所有实现类,本课时将深入分析这些 BootService 实现类的具体逻辑:

网络连接管理

在前面的介绍中提到 SkyWalking Agent 会定期将收集到的 JVM 监控和 Trace 数据定期发送到后端的 OAP 集群,GRPCChannelManager 负责维护 Agent 与后端 OAP 集群通信时使用的网络连接。这里首先说一下 gRPC 里面的两个组件:

  • **ManagedChanne l:它是 gRPC 客户端的核心类之一,它逻辑上表示一个 Channel,底层持有一个 TCP 链接,并负责维护此连接的活性。也就是说,在 RPC 调用的任何时机,如果检测到底层连接处于关闭状态(terminated),将会尝试重建连接。通常情况下,我们不需要在 RPC 调用结束后就关闭 Channel ,该 Channel 可以被一直重用,直到整个客户端程序关闭。当然,我们可以在客户端内以连接池的方式使用多个 ManagedChannel ,在每次 RPC 请求时选择使用轮训或是随机等算法选择一个可用的 Channel,这样可以提高客户端整体的并发能力。
  • **ManagedChannelBuilder:**它负责创建客户端 Channel,ManagedChannelBuilder 使用了 provider 机制,具体是创建了哪种 Channel 由 provider 决定,常用的 ManagedChannelBuilder 有三种:NettyChannelBuilder、OkHttpChannelBuilder、InProcessChannelBuilder,如下图所示:

在 SkyWalking Agent 中用的是 NettyChannelBuilder,其创建的 Channel 底层是基于 Netty 实现的。OkHttpChannelBuilder 创建的 Channel 底层是基于 OkHttp 库实现的。InProcessChannelBuilder 用于创建进程内通信使用的 Channel。

SkyWalking 在 ManagedChannel 的基础上封装了自己的 Channel 实现 —— GRPCChannel ,可以添加一些装饰器,目前就只有一个验证权限的修饰器,实现比较简单,这里就不展开分析了。

介绍完基础知识之后,回到 GRPCChannelManager,其中维护了一个 GRPCChannel 连接以及注册在其上的 Listener 监听,另外还会维护一个后台线程定期检测该 GRPCChannel 的连接状态,如果发现连接断开,则会进行重连。GRPCChannelManager 的核心字段如下:

// 封装了上面介绍的gRPC Channel
private volatile GRPCChannel managedChannel = null// 定时检查 GRPCChannel的连接状态重连gRPC Server的定时任务
private volatile ScheduledFuture<?> connectCheckFuture; 
// 是否重连。当 GRPCChannel断开时会标记 reconnect为 true,后台线程会根据该标
// 识决定是否进行重连
private volatile boolean reconnect = true// 加在 Channel上的监听器,主要是监听 Channel的状态变化
private List<GRPCChannelListener> listeners;
 // 可选的 gRPC Server集合,即后端OAP集群中各个OAP实例的地址
private volatile List<String> grpcServers;

前文介绍 ServiceManager 时提到,Agent 启动过程中会依次调用 BootService 实现的 prepare() 方法 → boot() 方法 → onComplete() 方法之后,才能真正对外提供服务。GRPCChannelManager 的 prepare() 方法 、onComplete() 方法都是空实现,在 boot() 方法中首先会解析 agent.config 配置文件指定的后端 OAP 实例地址初始化 grpcServers 字段,然后会初始化这个定时任务,初次会立即执行,之后每隔 30s 执行一次,具体执行的就是 GRPCChannelManager.run() 方法,其核心逻辑是检测链接状态并适时重连:

public void run() {
    if (reconnect && grpcServers.size() > 0) {
        // 根据配置,连接指定OAP实例的IP和端口
        managedChannel = GRPCChannel.newBuilder(ipAndPort[0], 
                Integer.parseInt(ipAndPort[1]))
            .addManagedChannelBuilder(new StandardChannelBuilder())
            .addManagedChannelBuilder(new TLSChannelBuilder())
            .addChannelDecorator(new AuthenticationDecorator())
            .build();
        // notify()方法会循环调用所有注册在当前连接上的GRPCChannelListener实
        // 例(记录在listeners集合中)的statusChanged()方法,通知它们连接创建
        // 成功的事件
        notify(GRPCChannelStatus.CONNECTED);
        // 设置 reconnect字段为false,暂时不会再重建连接了
        reconnect = false;
    }
}

GRPCChannelListener 是一个监听器接口,有多个需要发送网络请求的 BootService 实现类同时实现了该接口,如下图所示,后面会详细介绍这些 BootService 实现类的具体功能。

最后,GRPCChannelManager 对外提供了 reportError() 方法,在其他依赖该网络连接的 BootService 实现发送请求失败时,可以通过该方法将 reconnect 字段设置为 true,并由后台线程重新创建 GRPCChannel。

注册协议及实现

介绍完 Agent 与后端 OAP 集群基本的连接方式之后,还需要了解 Agent 和 Server 交互的流程和协议。在 Agent 与后端 OAP 创建连接成功后的第一件事是进行注册流程。

首先来介绍注册协议涉及的 proto 的定义—— Register.proto 文件,其位置如下:

Register.proto 中定义了 Register 服务,这里先关注 doServiceRegister() 和 doServiceInstanceRegister() 两个 RPC 接口,如下所示:

service Register {
    rpc doServiceRegister(Services)returns(ServiceRegisterMapping) {}

    rpc doServiceInstanceRegister (ServiceInstances) returns
        (ServiceInstanceRegisterMapping) {}

    # 还有三个方法,这里省略一下,后面会介绍
}

  • doServiceRegister() 接口:将服务(Service)的名称注册到后端的 OAP 集群。参数 Services 中会携带当前服务的名称(其中还可以附加一些 KV 格式的信息);返回的 ServiceRegisterMapping 其实是多个 KV,其中就包含了后端 OAP 服务端生成的ServiceId。
  • doServiceInstanceRegister() 接口:将服务实例(ServiceInstance)的名称注册到后端 OAP 集群。参数 ServiceInstances 中会携带服务实例(ServiceInstance)的名称,以及 serviceId、时间戳等信息;返回的 ServiceInstanceRegisterMapping 本质也是一堆 KV,其中包含 OAP 为服务实例生成的 ServiceInstanceId。

Service、ServiceInstance 的概念可以回顾本课程的第一课时“Skywalking 初体验”。

与 Service 注册流程相关的 BootService 实现是 ServiceAndEndpointRegisterClient。ServiceAndEndpointRegisterClient 实现了 GRPCChannelListener 接口,在其 prepare() 方法中首先会将其注册到 GRPCChannelManager 来监听网络连接,然后生成当前 ServiceInstance 的唯一标识:

public void prepare() throws Throwable {
    // 查找 GRPCChannelManager实例(前面介的ServiceManager.bootedServices
    // 集合会按照类型维护BootService实例,查找也是查找该集合),然后将 
    // ServiceAndEndpointRegisterClient注册成Listener
    ServiceManager.INSTANCE.findService(GRPCChannelManager.class)
        .addChannelListener(this); 
    // 确定INSTANCE_UUID,优先使用gent.config文件中配置的INSTANCE_UUID,
    // 若未配置则随机生成
    INSTANCE_UUID = StringUtil.isEmpty(Config.Agent.INSTANCE_UUID) ? 
         UUID.randomUUID().toString().replaceAll("-""") : 
         Config.Agent.INSTANCE_UUID;
}

在网络连接建立之后,会通知其 statusChanged() 方法更新 registerBlockingStub 字段:

public void statusChanged(GRPCChannelStatus status) {
    if (GRPCChannelStatus.CONNECTED.equals(status)) { 
        // 网络连接创建成功时,会依赖该连接创建两个stub客户端
        Channel channel = ServiceManager.INSTANCE.findService(
                GRPCChannelManager.class).getChannel();
        registerBlockingStub = RegisterGrpc.newBlockingStub(channel);
        serviceInstancePingStub = ServiceInstancePingGrpc
             .newBlockingStub(channel);
    } else { // 网络连接断开时,更新两个stub字段(它们都是volatile修饰)
        registerBlockingStub = null;
        serviceInstancePingStub = null;
    }
    this.status = status; // 更新status字段,记录网络状态
}

registerBlockingStub 是 gRPC 框架生成的一个客户端辅助类,可以帮助我们轻松的完成请求的序列化、数据发送以及响应的反序列化。gRPC 的基础使用这里不再展开。

ServiceAndEndpointRegisterClient 也同时实现了 Runnable 接口,在 boot() 方法中会启动一个定时任务,默认每 3s 执行一次其 run() 方法,该定时任务首先会通过 doServiceRegister() 接口完成 Service 注册,相关实现片段如下:

while (GRPCChannelStatus.CONNECTED.equals(status) && shouldTry) {
    shouldTry = false;
    // 检测当前Agent是否已完成了Service注册
    if (RemoteDownstreamConfig.Agent.SERVICE_ID == 
            DictionaryUtil.nullValue()) {
        if (registerBlockingStub != null) { // 第二次检查网络状态
            // 通过doServiceRegister()接口进行Service注册
            ServiceRegisterMapping serviceRegisterMapping = 
              registerBlockingStub.doServiceRegister(
                Services.newBuilder().addServices(Service.newBuilder()
                 .setServiceName(Config.Agent.SERVICE_NAME)).build());
            for (KeyIntValuePair registered : 
               serviceRegisterMapping.getServicesList()) {// 遍历所有KV
                if (Config.Agent.SERVICE_NAME
                          .equals(registered.getKey())) { 
                    RemoteDownstreamConfig.Agent.SERVICE_ID = 
                           registered.getValue(); // 记录serviceId
                    // 设置shouldTry,紧跟着会执行服务实例注册
                    shouldTry = true; 
                }
            }
        }
    } else { // 后续会执行服务实例注册以及心跳操作
        ... ... 
    }
}

通过分析这段代码,我们可以知道:

  • 如果在 agent.config 配置文件中直接配置了 serviceId 是无需进行服务注册的。
  • ServiceAndEndpointRegisterClient 会根据监听 GRPCChannel 的连接状态,决定是否发送服务注册请求、服务实例注册请求以及心跳请求。

完成服务(Service)注册之后,ServiceAndEndpointRegisterClient 定时任务会立即进行服务实例(ServiceInstance)注册,具体逻辑如下,逻辑与服务注册非常类似:

while (GRPCChannelStatus.CONNECTED.equals(status) && shouldTry) {
    if (RemoteDownstreamConfig.Agent.SERVICE_ID == 
            DictionaryUtil.nullValue()) {
        ... // 省略服务注册逻辑
    } else {
        if (RemoteDownstreamConfig.Agent.SERVICE_INSTANCE_ID == 
                   DictionaryUtil.nullValue()) {
            // 调用 doServiceInstanceRegister()接口,用serviceId和
            // INSTANCE_UUID换取SERVICE_INSTANCE_ID
            ServiceInstanceRegisterMapping instanceMapping =
                  registerBlockingStub.doServiceInstanceRegister(
                 ServiceInstances.newBuilder()
                .addInstances(ServiceInstance.newBuilder()
                .setServiceId(RemoteDownstreamConfig.Agent.SERVICE_ID)
                // 除了serviceId,还会传递uuid、时间戳以及系统信息之类的
                .setInstanceUUID(INSTANCE_UUID)
                .setTime(System.currentTimeMillis())
                .addAllProperties(OSUtil.buildOSInfo())).build());
            for (KeyIntValuePair serviceInstance : 
                    instanceMapping.getServiceInstancesList()) {
                if (INSTANCE_UUID.equals(serviceInstance.getKey())) {
                    // 记录serviceIntanceId
                    RemoteDownstreamConfig.Agent.SERVICE_INSTANCE_ID =
                        serviceInstance.getValue();
                }
            }
        }else{
            ... // 省略心跳的相关逻辑
        }
    }
}

心跳协议及实现

在完成服务注册以及服务实例操作之后,ServiceAndEndpointRegisterClient 会开始定期发送心跳请求,通知后端 OAP 服务当前 Agent 在线。心跳涉及一个新的 gRPC 接口如下(定义在 InstancePing.proto 文件中):

service ServiceInstancePing {
    rpc doPing (ServiceInstancePingPkg) returns (Commands) {}
}

ServiceAndEndpointRegisterClient 中心跳请求相关的代码片段如下,心跳请求会携带serviceInstanceId、时间戳、INSTANCE_UUID 三个信息:

while (GRPCChannelStatus.CONNECTED.equals(status) && shouldTry) {
    if (Agent.SERVICE_ID == DictionaryUtil.nullValue()) {
        ... // 省略服务注册逻辑
    } else {
        if (Agent.SERVICE_INSTANCE_ID == DictionaryUtil.nullValue()) {
            ... // 省略服务实例注册逻辑
        }else// 并没有对心跳请求的响应做处理
            serviceInstancePingStub.doPing(ServiceInstancePingPkg
               .newBuilder().setServiceInstanceId(SERVICE_INSTANCE_ID)
               .setTime(System.currentTimeMillis())
               .setServiceInstanceUUID(INSTANCE_UUID).build());
        }
    }
}

Endpoint、NetWorkAddress 同步

在前文介绍 SkyWalking 基本使用时看到,Trace 数据中包含了请求的 URL 地址、RPC 接口名称、HTTP 服务或 RPC 服务的地址、数据库的 IP 以及端口等信息,这些信息在整个服务上线稳定之后,不会经常发生变动。而在海量 Trace 中包含这些重复的字符串,会非常浪费网络带宽以及存储资源,常见的解决方案是将字符串映射成数字编号并维护一张映射表,在传输、存储时使用映射后的数字编号,在展示时根据映射表查询真正的字符串进行展示即可。这类似于编码、解码的思想。SkyWalking 也是如此处理 Trace 中重复字符串的。

SkyWalking 中有两个 DictionaryManager:

  • EndpointNameDictionary:用于同步 Endpoint 字符串的映射关系。
  • NetworkAddressDictionary:用于同步网络地址的映射关系。

EndpointNameDictionary 中维护了两个集合:

  • endpointDictionary:记录已知的 Endpoint 名称映射的数字编号。
  • unRegisterEndpoints:记录了未知的 Endpoint 名称。

EndpointNameDictionary 对外提供了两类操作,一个是查询操作(核心实现在 find0() 方法),在查询时首先去 endpointDictionary 集合查找指定 Endpoint 名称是否已存在相应的数字编号,若存在则正常返回数字编号,若不存在则记录到 unRegisterEndpoints 集合中。为了防止占用过多内存导致频繁 GC,endpointDictionary 和 unRegisterEndpoints 集合大小是有上限的(默认两者之和为 10^7)。

另一个操作是同步操作(核心实现在 syncRemoteDictionary() 方法),在 ServiceAndEndpointRegisterClient 收到心跳响应之后,会将 unRegisterEndpoints 集合中未知的 Endpoint 名称发送到 OAP 集群,然后由 OAP 集群统一分配相应的数字编码,具体实现如下:

public void syncRemoteDictionary(RegisterGrpc.RegisterBlockingStub 
          serviceNameDiscoveryServiceBlockingStub) {
    // 创建请求,每个Endpoint中都封装了 Endpoint名称以及关联的serviceId
    Endpoints.Builder builder = Endpoints.newBuilder();
    for (OperationNameKey operationNameKey : unRegisterEndpoints) {
        Endpoint endpoint = Endpoint.newBuilder()
            .setServiceId(operationNameKey.getServiceId())
            .setEndpointName(operationNameKey.getEndpointName())
            .setFrom(operationNameKey.getSpanType()) 
            .build();
        builder.addEndpoints(endpoint);
    }
    // 发送同步请求
    EndpointMapping serviceNameMappingCollection = 
       serviceNameDiscoveryServiceBlockingStub
           .doEndpointRegister(builder.build());
    for (EndpointMappingElement element : 
         serviceNameMappingCollection.getElementsList()) {
        // 将返回的映射关系,记录到 endpointDictionary集合中,并从 
        // unRegisterEndpoints集合中删除Endpoint信息
        OperationNameKey key = new OperationNameKey(
            element.getServiceId(), element.getEndpointName(),
            DetectPoint.server.equals(element.getFrom()),
            DetectPoint.client.equals(element.getFrom()));
        unRegisterEndpoints.remove(key);
        endpointDictionary.put(key, element.getEndpointId());
    }
}

NetworkAddressDictionary 实现与 EndpointNameDictionary 类似,就留给你自己进行分析,这里不再展开。

收集 JVM 监控

在前面介绍 SkyWalking Rocketbot 时看到 SkyWalking 可以监控服务实例的 CPU、堆内存使用情况以及 GC 信息,这些信息都是通过 JVMService 收集的。JVMService 也实现了 BootService 接口。JVMService 的结构大致如下图所示:

在 JVMService 中会启动一个独立 collectMetric 线程请求 MXBean 来收集 JVM 的监控数据,然后将监控数据保存到 queue 队列(LinkedBlockingQueue 类型)中缓存,之后会启动另一个线程读取 queue 队列并将监控数据通过 gRPC 请求发送到后端的 OAP 集群。

在 JVMService.boot() 方法的实现中会初始化 queue 缓冲队列以及 Sender 对象,如下:

public void prepare() throws Throwable {
    queue = new LinkedBlockingQueue(Config.Jvm.BUFFER_SIZE);
    sender = new Sender();
    ServiceManager.INSTANCE.findService(GRPCChannelManager.class)
         .addChannelListener(sender)// sender会监听底层的连接状态
}

在 Sender.run() 方法中封装了读取 queue 队列以及发送 gRPC 请求的逻辑,其中会根据底层的 GRPCChannel 连接状态、当前服务以及服务实例的注册情况决定该任务是否执行。Sender.run() 方法的核心逻辑如下(省略全部边界检查逻辑):

JVMMetricCollection.Builder builder = 
           JVMMetricCollection.newBuilder();
LinkedList<JVMMetric> buffer = new LinkedList<JVMMetric>();
// 将 queue队列中缓存的全部监控数据填充到 buffer中
queue.drainTo(buffer);
// 创建 gRPC请求参数
builder.addAllMetrics(buffer);
builder.setServiceInstanceId(Agent.SERVICE_INSTANCE_ID); // 
// 通过 gRPC调用将JVM监控数据发送到后端 OAP集群
stub.collect(builder.build());

简单看一下该 gRPC 接口以及参数的定义,非常简单:

service JVMMetricReportService {
    rpc collect (JVMMetricCollection) returns (Commands) {}
}

在 JVMMetricCollection 中封装了 ServiceInstanceId 以及 JVMMetric 集合,JVMMetric 封装了一个时刻抓取到的 CPU、堆内存使用情况以及 GC 等信息,这里不再展开。

了解了发送过程之后,我们再来看收集 JVM 监控的原理。在 collectMetric 线程中会定期执行 JVMService.run() 方法,其中通过 MXBean 抓取 JVM 监控信息,然后填充到 JVMMetric 对象中并记录到 queue 缓冲队列中,大致实现如下:

// 通过JMX获取CPU、Memory、GC的信息,然后组装成JVMMetric
JVMMetric.Builder jvmBuilder = JVMMetric.newBuilder();
jvmBuilder.setTime(currentTimeMillis);
// 通过 MXBean获取 CPU、内存以及GC相关的信息,并填充到 JVMMetric
jvmBuilder.setCpu(CPUProvider.INSTANCE.getCpuMetric());
jvmBuilder.addAllMemory(
     MemoryProvider.INSTANCE.getMemoryMetricList());
jvmBuilder.addAllMemoryPool(
     MemoryPoolProvider.INSTANCE.getMemoryPoolMetricsList());
jvmBuilder.addAllGc(GCProvider.INSTANCE.getGCList());
JVMMetric jvmMetric = jvmBuilder.build();
// 将JVMMetric写入到queue缓冲队列中
if (!queue.offer(jvmMetric)) { // queue缓冲队列的长度默认为600
    queue.poll(); // 如果queue队列被填满,则抛弃最老的监控信息,保留最新的
    queue.offer(jvmMetric);
}

这里深入介绍一下 JDK 提供的 MXBean 是如何使用的。我们以 GCProvider 为例进行分析(获取其他监控指标的方式也是类似的)。GCProvider 通过枚举方式实现了单例,在其构造方法中,会先获取 MXBean 并封装成相应的 GCMetricAccessor 实现,大致实现如下:

// 获取GC相关的MXBean
beans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean bean : beans) {
    String name = bean.getName();
    // 解析MXBean的名称即可得知当前使用的是哪种垃圾收集器,我们就可以创建相应
    // 的GCMetricAccessor实现
    GCMetricAccessor accessor = findByBeanName(name);
    if (accessor != null) {
        metricAccessor = accessor;
        break;
    }
}

GCMetricAccessor 接口的实现类如下图所示,针对每一种垃圾收集器都有相应的实现:

GCMetricAccessor 接口中提供了一个 getGCList() 方法用于读取 MXBean 获取 GC 的信息,在抽象类 GCModule 中实现了 getGCList() 方法的核心逻辑:

public List<GC> getGCList() {
    List<GC> gcList = new LinkedList<GC>();
    for (GarbageCollectorMXBean bean : beans) {
        String name = bean.getName();
        GCPhrase phrase;
        long gcCount = 0;
        long gcTime = 0;
        // 下面根据 MXBean的名称判断具体的GC信息
        if (name.equals(getNewGCName())) { // Young GC的信息
            phrase = GCPhrase.NEW;
            // 计算GC次数,从MXBean直接拿到的是GC总次数
            long collectionCount = bean.getCollectionCount();
            gcCount = collectionCount - lastYGCCount;
            lastYGCCount = collectionCount; // 更新 lastYGCCount
            // 计算GC时间,从 MXBean直接拿到的是GC总时间
            long time = bean.getCollectionTime();
            gcTime = time - lastYGCCollectionTime;
            lastYGCCollectionTime = time; // 更新lastYGCCollectionTime
        } else if (name.equals(getOldGCName())) { // Old GC的信息
            phrase = GCPhrase.OLD;
            ... ... // Old GC的计算方式与Young GC的计算方式相同,不再重复
        } 
        gcList.add( // 最后将 GC信息封装成List返回
            GC.newBuilder().setPhrase(phrase)
                .setCount(gcCount).setTime(gcTime).build()
        );
    }
    return gcList;
}

在不同类型的垃圾收集器中,Young GC、Old GC 的名称不太相同(例如,G1 中叫 G1 Young Generation 和 G1 Old Generation,在 CMS 中则叫 ParNew 和 ConcurrentMarkSweep),所以这里调用的 getNewGCName() 方法和 getOldGCName() 方法都是抽象方法,延迟到了 GCModule 子类中才真正实现,这是典型的模板方法模式。

其他

BootService 接口的剩余四个实现与 Trace 数据的收集和发送相关,在后面的课时中会详细展开分析,这里简单说一下它们的核心功能。

  • ContextManager:负责管理一个 SkyWalking Agent 中所有的 Context 对象。
  • ContextManagerExtendService:负责创建 Context 对象。
  • TraceSegmentServiceClient:负责将 Trace 数据序列化并发送到 OAP 集群。
  • SamplingService:负责实现 Trace 的采样。

总结

本课时深入介绍了 BootService 的三个核心实现。 GRPCChannelManager 负责管理 Agent 到 OAP 集群的网络连接,并实时的通知注册的 Listener。ServiceAndEndpointRegisterClient 的核心功能有三项。

  1. 注册功能:其中包括服务注册和服务实例注册两次请求。
  2. 定期发送心跳请求:与后端 OAP 集群维持定期探活,让后端 OAP 集群知道该 Agent 正常在线。
  3. 定期同步 Endpoint 名称以及网络地址:维护当前 Agent 中字符串与数字编号的映射关系,减少后续 Trace 数据传输的网络压力,提高请求的有效负载。

JVMService 负责定期请求 MXBean 获取 JVM 监控信息并发送到后端的 OAP 集群。

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

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

相关文章

基于Java+Swing实现雷电小游戏

基于JavaSwing实现雷电小游戏 一、系统介绍二、功能展示三、其他系统四、获取源码 一、系统介绍 基于java的雷电游戏基本功能包括&#xff1a;敌方飞机随机飞行、我方飞机手动控制飞行&#xff0c;射击比拼&#xff0c;游戏闯关等。本系统结构如下&#xff1a; &#xff08;1&…

Java中线程的创建与使用、Thread类的常用方法

1、什么是进程与线程 1.1 含义 1.1.1 进程 进程是指正在运行的程序的实例。在操作系统中&#xff0c;一个进程代表了一个正在执行的程序&#xff0c;它包括了程序的代码、数据以及程序执行时所需要的系统资源。 最直观的就是我们任务管理器&#xff1a; 任务管理器中的每一…

Centos7安装和配置Mysql5.7

第一步&#xff1a;获取mysql YUM源 进入mysql官网获取RPM包下载地址&#xff0c;下面就是mysql5.7的Yum仓库的rpm包&#xff1a; mysql5.7链接地址&#xff1a; https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm 第二步&#xff1a;下载和安装mysql…

卷福的十年同学会

1.一通电话 某个上班日的午休时间里&#xff0c;小卷正趴在办公桌玩着手机准备睡一会&#xff0c;“叮咚”&#xff0c;一条微信消息弹出来&#xff0c;是大学的班群消息。 “五一期间大家来学校聚一下吧&#xff0c;今年是我们成为同学的十年了&#xff0c;大家提前报名哦&a…

Qt设置软件启动动画(支持图片和视频俩种方式)

目录 软件启动动画效果静态背景动态背景 程序启动动画QSplashScreen启动时加载静态图片启动时加载视频动画将启动动画置于所有窗口顶层 软件启动动画效果 先来看效果。下面录制了加载图片和gif动图的俩种效果。 静态背景 动态背景 这里我加载了一个gif的动图&#xff0c;你也…

AMBA AHB的burst termination

前言 在AMBA AHB协议中&#xff0c;AHB master可以用burst传输连续取多笔数据。AHB定义了4、8和16拍的burst传输、未定义长度的burst传输和单次传输。Burst传输中支持incrementing和wrapping。 Incrementing burst用于访问顺序的memory地址&#xff0c;burst中每个拍的地址都…

数据压缩的常用手段以及方法

0. 简介 之前我们在《经典文献阅读之–R-PCC(基于距离图像的点云压缩方法)》中提到了&#xff0c;我们可以通过一些算法层面来完成数据的压缩&#xff0c;而其实更简单或者说更直接的方法就是使用half这种形式来完成数据压缩。 1. half和float Half是用16位表示浮点数的一种…

什么是 FL Studio?2023年最新版 FL Studio21.0.3.3517中文版图文安装教程

什么是 FL Studio&#xff1f; FL Studio 是一个数字音频工作站 (DAW)。该软件借助各种编辑工具、插件和效果&#xff0c;让您可以录制、混音和掌握高度复杂的音乐作品。FL Studio 还允许您注册和编辑 MIDI 文件&#xff0c;您可以在众多可用乐器之一上演奏这些文件。FL Studi…

树莓派 python3.9降级为python3.7

今天烧录了一个官方烧录器中的最新的镜像&#xff0c;打开之后python的版本是3.9的&#xff0c;之前做的一些东西都是基于python3.7的&#xff0c;再重新架构十分麻烦&#xff0c;于是干脆就把python3.9进行降级&#xff0c;降为python3.7. 这个镜像不像之前的一些镜像&#x…

通用商城项目(上)

通用型产品&#xff08;电商&#xff09;发布解决方案落地实现&#xff08;基于分布式微服务技术栈&#xff1a; SpringBootSpring CloudSpring Cloud Alibaba VueElementUl MyBatis-Plus MySQL Git Maven Linux Nginx Docker 前后端分离&#xff09; 项目技术栈和前置技术 项…

【软件设计师暴击考点】操作系统知识高频考点暴击系列【一】

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;软件…

Web网页制作-知识点(1)——HTML5介绍、HTML5的DOCTYPE声明、HTML基本骨架、标题标签、段落 换行、水平线图片图片路径、超链接

目录 HTML5介绍 HTML5的DOCTYPE声明 HTML基本骨架 标题标签 段落、换行、水平线 图片 图片路径* 超链接 HTML5介绍 HTML5是用来描述网页的一种语言&#xff0c;被称为超文本标记语言。用HTML5编写的文件&#xff0c;后缀以.html结尾 HTML是一种标记语言&#xff0c;标…

自动化神器AutoIt,告别重复劳动

概要 计算机已经进入大众家庭多年&#xff0c;它给我们带来了便利&#xff0c;却也带来了枯燥、重复、机械的重复工作。今天&#xff0c;我要和大家分享一款自动化工具AutoIt&#xff0c;它能够帮助你告别这些烦恼&#xff0c;并提高工作效率。 AutoIt 是一款完全免费的Windows…

leetcode82. 删除排序链表中的重复元素 II(java)

删除排序链表中的重复元素 leetcode82. 删除排序链表中的重复元素 II题目描述一次遍历代码演示 链表专题 leetcode82. 删除排序链表中的重复元素 II 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/remove-duplicates-fr…

1.1 编写一个简单的C++程序

博主介绍&#xff1a;爱打游戏的计算机专业学生 博主主页&#xff1a;夏驰和徐策 所属专栏&#xff1a;夏驰和徐策带你从零开始学C 1.1.0 这段话告诉我们什么&#xff1f; 这段话解释了一个C程序中的main函数的基本结构和功能。 它告诉我们以下几点&#xff1a; 1. C程序的…

Debian11 编译bluez

之前的几篇文章写过如果编译x86和dv300 版本的bluez&#xff0c;不过那都是在 Centos7 上编译的。然而当我从taobao 上买了一个蓝牙适配器后发现无法使用&#xff08;淘宝客服说不支持Centos&#xff0c;只支持ubuntu 和 debian&#xff09;。再者 Centos 现在也停止支持服务了…

头歌网页设计与制作实训答案

我这里已经看不见原题目了&#xff0c;只粘贴了有Begin和End部分的代码&#xff0c;如果题目符合但答案不符合的的&#xff0c;欢迎在评论区找我。如果有帮助&#xff0c;请赞一个。注意看目录里有没有你需要的。 目录 一、HTML——基础 1.初识HTML: 简单的Hello World网页制…

程序员秋招最全Java面试题及答案整理(2023最新版)

前言 大家好&#xff0c;最近一个月&#xff0c;花了不少时间&#xff0c;给大家整理了一套 2023 的技术面试资料 包括各大厂最新面试题以及面经解析涉及JVM&#xff0c;Mysql&#xff0c;并发&#xff0c;Spring&#xff0c;Mybatis&#xff0c;Redis&#xff0c;RocketMQ&a…

Firefly-SRC资产探测平台

前言 Firefly是一个集资产管理、信息收集和漏洞扫描的综合平台。 Firefly-SRC依托于Firefly平台中的信息收集功能&#xff0c;不断收集和整理各大src相关资产数据。希望能为各位白帽子师傅们提供更稳定可靠的src资产数据&#xff0c;减少师傅挖洞前期的信息收集时间&#xff0c…

一体化个人门户Web Portal

什么是 Web Portal ? Web Portal 是一个一体化的 Web 仪表板&#xff0c;提供许多小部件来构建个人门户。具有加载外部插件的能力。对于那些只需要链接仪表板并希望使用 yaml 配置它的人来说&#xff0c;Lite 版是一个精简版本。 构建镜像 如果你不想自己构建&#xff0c;可以…