linux内核网络分层概述

在开发应用时,我们使用 socket 实现网络数据的收发。以tcp为例,server端通过 socket, bind, listen来创建服务端,然后通过 accept接收客户端连接;客户端通过 socket和 connect系统调用来创建客户端。用于数据收发的系统调用包括 send, recv, sendto, recvfrom等。除了上述系统调用之外,另外还有多路复用技术 select,poll, epoll,也常常在网络应用中使用。

    // tcp 服务端

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr))}
    listen(listen_fd, 5);

    int accetp_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client);
    
     // tcp 客户端
     sock_fd = socket(AF_INET, SOCK_STREAM, 0);
     connect(sock_fd, (struct sockaddr *)&addr_serv,sizeof(struct sockaddr));

在做应用开发时,使用上述系统调用比较简单和简洁。对于 linux 网络来说,这些系统调用只是冰山一角,在 linux 网络收发包的过程中,报文要经过协议栈,网卡驱动以及网卡硬件本身的处理。

1网络分层

如下是 linux 收发包示意图:

上图中包括的环节有网卡,网卡驱动,网络层,传输层,socket 层,应用层。收包时报文的流动方向是从下到上,发包时反之。其中网络应用工作在用户态;传输层、网络层以及网卡驱动工作在内核态;socket 层是 linux 提供的系统调用,是用户态和内核态的桥梁。应用层、socket、传输层网络层是软件部分,网卡是硬件部分,网卡驱动是软件与硬件的桥梁

1.1网卡

dma

dma即直接内存访问,直接内存访问的意思是硬件来访问内存,数据读写过程中不需要cpu参与。在收包方向,网卡收到包之后,会通过dma引擎将数据保存在主机内存中,然后将收包事件通知给 cpu,之后cpu便可以处理这个报文;在发包方向,cpu将报文放到对应的内存buffer之后,将信息通知网卡,之后网卡便会通过dma引擎将buffer中的数据取出并发送出去。dma过程中, cpu和网卡之间只需要一些通知指令,cpu不需要参与数据的读写过程。设想一下,如果没有dma, 那么在收发包时就需要cpu不断地从网卡中读写数据进行收发包,这种效率会非常低下。

环形队列

环形队列中的元素称为bd(buffer descriptor),是一个描述符,该描述符中存储的并不是真正的报文数据,而是报文的元数据。考虑最简单的情况,bd中应包括一个buffer指针,指向实际存储数据的内存地址,另外还有一个变量是内存buffer的长度。实际中bd的结构会更复杂,比如一个报文太长,在一个buffer中存不下,这样就需要有其它的信息,比如报文存了几个buffer;另外网卡收到数据之后,并不一定从buffer的开始处存储报文数据,这样就需要在buffer的开始处留出一段空间,有时还需要在buffer的末尾处保留一段空间。环形队列是cpu软件和网卡硬件通信的桥梁,网卡硬件和cpu都可以访问环形队列。

struct buffer_desc {
    char *buffer;
    int length;
};

1.2网卡驱动

网卡驱动与网卡通信的方式有两种:中断和轮询,这也是软件和硬件交互的两种方式。以收包为例,对于中断方式,即网卡收到数据包之后,会触发一个中断,然后网卡驱动注册的中断处理程序便会从网卡中读取数据并做后续的处理;轮询方式是cpu不断地查询网卡的相关寄存器,判断当前有没有新的包需要处理,有则处理,没有则这次轮询空转。当流量较小的时候,在没有数据时,轮询方式会导致cpu空跑,浪费cpu资源,所以轮询方式适用于流量较大的场景。

中断提高实时性(因为在linux中,中断的优先级最高,高于线程和软中断,中断到来之后会立即得到响应),但是如果在流量比较大的场景,网卡产生中断的速度就会非常快,这样会让cpu为处理中断事件浪费所有的时间。

轮询方式适用于网络流量比较大的场景,比如路由器,路由器是通信专用设备,功能比较单一,核心功能就是处理网络流量,所以可以使用轮询方式(即使短时间内流量小导致cpu空转也不会造成其它影响,因为路由器上也没有其它任务来抢cpu)。DPDK中就使用了轮询方式。

linux内核中提供了napi方式,该方式既不是纯中断方式,也不是纯轮询方式,而是集合了中断和轮询,发挥了两种方式的优点。当中断到来时,napi便会关中断,然后处理包,因为关了中断,如果在napi处理包的过程中又来了新的报文,napi处理过程就不会被打断。napi每一次处理均是批量处理,不是处理一个报文就返回,也就是说在napi处理过程中,虽然新到的数据包没有中断,napi也会处理它。

napi的退出机制:napi并不是一直在轮询,当接收队列中的报文都被处理完毕,当然就会返回;但是如果接收队列中的报文非常多,短时间内没有处理完,会一直处理报文吗 ?不会的,为了防止处理网络报文的任务一直占用这cpu,napi有主动退出机制,一个是时间维度,一个是报文数量维度,时间维度是 napi 处理时间超过某个时间便会返回,未处理的报文等到下次调度时再次处理,数量维度是处理的报文数量达到一定的数量时,也会主动返回,未处理的包等到下次调度时再次处理。

napi —— linux 网卡驱动收包机制-CSDN博客

1.3网络层

网络层,最常用的是ip。

ip层主要的作用即路由,在收包方向上根据目的ip决定报文是接收并上传到传输层(目的ip是本地 ip),还是转发(目的ip不是本地ip)。

另外 ip 层也需要处理分片,之所以处理分片,是因为报文的长度大于 mtu。

linux中的netfilter功能也是在ip层实现。

netfilter_netfilter模块报文处理-CSDN博客

1.4传输层

传输层包括两个协议,tcp和udp。

tcp有如下3个特点:

1.4.1面向连接

tcp的服务端和客户端通信之前需要建立连接,建立连接之后才可以收发数据,收发数据结束之后需要断开连接。建立连接过程需要3次握手,断开连接过程中需要4次挥手。

tcp断开连接时,可能是4次挥手,也可能是3次挥手。3次挥手的时候,第二次挥手和第三次挥手合并到了一个报文中。

1.4.2延时ack

如果收到数据之后就立即发送ack的话,那么会导致网络带宽利用率低,因为ack报文没有有效数据,只有tcp, ip协议头。

延时ack是在收到数据之后不立即发送ack,而是等待一定的时间(最小等待时间是40ms,最大等待时间是200ms) 再发送ack;如果在等待期间,本端有数据要发送,那么ack也会跟着数据一块发送出去,不会受40ms和200ms的约束,比如收到数据之后开始等待,等待了10ms,还没到最小等待时间 40ms,如果这个时候要发送数据,那么就会停止等待(停止定时器),ack随数据一块发送出去。

延时ack通过定时器来实现,收到数据之后启动一个定时器,定时器的超时时间,最小是40ms,最大是200ms,如果定时器超时,就会发送ack;如果在定时器超时之前,ack随着本端数据一块发送了出去,那么定时器就会被取消。

延时ack的最小等待时间和最大等待时间,用两个宏来表示。

#define TCP_DELACK_MAX  ((unsigned)(HZ/5))  /* maximal time to delay before sending an ACK */
#define TCP_DELACK_MIN  ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */

因为延时ack的存在,被动关闭的一方在收到FIN报文的时候并不会立即发送ack,而是会有一定的延时。如果在等待的这段时间之内,没有数据要发送,本端也没有做其它动作(比如关闭连接),那么超时之后就会单独发送一个ack;如果本端还有数据发送,那么ack就会跟着数据一块发送出去。这种情况下就是4次挥手。

收到FIN之后,说明对端不会再发送新的数据到来,如果这个时候,本端接收完缓存的数据之后也调用了close将连接关闭,并且这个时候延时ack定时器还没有超时,那么ack就会和FIN一块发送出去。这种情况下就是3次挥手。

1.4.3FIN

当tcp连接的一方要关闭本端的发送时便会向对端发送FIN报文,FIN报文通过函数tcp_send_fin发送。

如果本端调用了close或者shutdown(fd, SHUT_WR) 之后就表示本端已经停止了发送,如果之后再调用send函数,那么会受到SIGPIPE信号,在这种情况下应用会被SIGPIPE杀死,如果不想应用被直接杀死,可以在send函数的最后一个参数中带上 MSG_NOSIGNAL 标志,这样就不会被杀死,而是返回错误码 “Broken pipe”。

3 次挥手:

// server 端
// 收到 FIN 之后,说明对端已经停止了发送数据
// 这个时候 read 返回 0 之后说明没有数据需要处理
// 此时立即关闭连接,调用 close 时会发送 FIN
// 因为延时 ack 还没有超时, 所以 ACK 和 FIN 一块发送出去
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>


#define PORT (12345)
#define DATA_MAX_LEN 1024

int main(int argc, char *argv[])
{
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_fd < 0)
    {
        printf("create listen socket error : %s\n", strerror(errno));
        return -1;
    }
    printf("tcp server listen fd: %d\n", listen_fd);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);
    if(bind(listen_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) < 0)
    {
        printf("bind error: %s\n", strerror(errno));
        return -1;
    }

    if(listen(listen_fd, 32))
    {
        printf("listen error: %s\n\a", strerror(errno));
        return -1;
    }

    struct sockaddr_in client_addr;
    socklen_t client_addrlen = sizeof(client_addr);
    int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addrlen);
    if(client_fd < 0) {
        printf("accept error: %s\n\a", strerror(errno));
        return -1;
    }
    printf("tcp server accept fd: %d\n", client_fd);

    char data[DATA_MAX_LEN] = {0};

    while(1) {
        int n = read(client_fd, data, DATA_MAX_LEN);
        if(n < 0) {
            printf("read error: %s\n\a", strerror(errno));
            break;
        } else if(n == 0) {
            // 读取数据之后立即关闭连接,这个时候延时 ack 还没有超时,
            // close 会导致本端也会向对端发送 FIN,所以此时 ACK 和 FIN 就会合并到一个报文中发送
            // 如果打开睡眠函数,让睡眠时间超过延时 ack 的时间,那么延时 ack 超时之后便会发送 ack
            // close 时的 FIN 会单独发送
            // usleep(220 * 1000);

            int value = 1;
            // 设置 QUICKACK, 的时候会立即发送一个 ack
            // QUICKACK 模式不是永久生效的,如果要想要保证每次收到数据之后都立即返回 ack,需要在每次收到数据之后,都要做一次这个设置
            // if (setsockopt(client_fd, IPPROTO_TCP, TCP_QUICKACK, (char *)&value, sizeof(int)) < 0) {
            //     printf("set quick ack error\n");
            //     return -1;
            // }
            close(client_fd);
            printf("client fd closed\n");
            break;
        }

        data[n] = 0;
        printf("received %d bytes: %s\n", n, data);
    }

    close(listen_fd);
    return 0;
}

// client 端
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define PORT (12345)

int main(int argc, char *argv[])
{
    int connect_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(connect_fd < 0)
    {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(PORT);
    if(connect(connect_fd, (struct sockaddr *)(&server_addr), sizeof(server_addr)) < 0)
    {
        printf("connect error: %s\n", strerror(errno));
        return -1;
    }

    char data[64] = "hello, server";
    int ret = send(connect_fd, data, strlen(data), 0);
    if(ret != strlen(data)) {
        printf("send data error: %s\n", strerror(errno));
        return -1;
    }

    sleep(2);
    close(connect_fd);
    return 0;
}

这是使用 tcpdump 抓包的结果截图,从下图中可以看到

第 6 个报文是客户端向服务端发送的 FIN,第一次挥手

第 7 个报文是服务端向客户端发送的 ACK + FIN 报文,第二次挥手

第 8 个报文是客户端向服务端发送的 ACK, 第三次挥手

4 次挥手:

如果我们将上述服务端的代码做一下修改,把第 69 行的 usleep(220 * 1000) 打开,这样的话当服务端 read 返回 0 之后不立即关闭 fd,而是延时一段时间再调用 close。延时时间是 220ms,超过了延时 ack 的最大时间,这样的话在 usleep() 期间,本端就会返回 ACK。之后调用 close,向对端发送 FIN。这样的话,第二次挥手和第 3 次挥手就不会合并。

抓包结果如下图所示

第 6 个报文是客户端向服务端发送的 FIN,第一次挥手

第 7 个报文是服务端向客户端发送的 ACK,第二次挥手,从第 6 和第 7 个报文的时间可以看出来,延时 ack 这次的延时在 45ms 左右

第 8 个报文,服务端向客户端发送的 FIN,第三次挥手

第 9 个报文,客户端向服务端发送的 ACK,第四次挥手

如果把上述服务端的代码做一下修改,把第 74 - 77 行的代码打开,这样在设置 quickack 的时候会发送 ack。然后再调用 close,发送 FIN,这样也是 4 次挥手。

从下图可以看出,第 6、7、8、9 报文是 4 次挥手的报文。

1.4.4字节流

tcp 是字节流协议,也就是说在 tcp 这一层,收发数据的边界与用户收发数据的边界可能会出现不一致。比如用户发送了一个长度为 2000 字节的报文,tcp 发送的时候可能会分两次发送,一次发送 1000; 如果用户先后发送了两个报文,长度分别是 1000 字节和 2000 字节,tcp 也可能第一次发送 500字节的报文,第二次发送 1000 字节的报文,第 3 次发送 1500 字节的报文, tcp 分 3 次包数据发送出去。

1.4.5可靠

tcp 最大的特点就是可靠性。丢包,乱序是导致不可靠的原因,tcp 可以通过序列号,重传等技术解决这样的问题,从而保证传输是可靠的。

从上边 3 个方面来说,udp 与 tcp 是相反的:

(1)没有连接

udp 的通信双方在通信之前,不需要建立连接。从这个角度来看,tcp 是面向连接的协议,是一对第一的通信方式,那么 tcp 只能支持单播,不支持多播和广播,因为多播和广播是一对多的通信方式;udp 支持多播和广播。

(2)数据报

udp 是数据报协议,也就是说数据收发的边界就是用户收发数据的边界,比如用户发送了 1000 字节的数据,那么 udp 就会发送 1000 字节的数据,udp 不会改变报文的边界。

(3)不可靠

udp 是不可靠的协议,如果报文在传输过程中出现了丢包或者乱序,udp 协议无法发现这些问题。如果使用 udp 协议,还要达到可靠的目标,可以在应用层实现。

1.4.6socket 层和应用层

本片文章值关注使用 socket 进行 tcp 通信。比较简单,不做太多记录。

2struct sk_buff

在内核网络代码中随处可见的一个变量名skb, 数据结构就是struct sk_buff(本文中也会使用skb来表示一个sk_buff)。无论是在哪一层,一个报文在 linux 内核中就用sk_buff 来表示,这方便在各个网络层之间交换数据,而不需要复制数据。

sk_buff 中的字段比较多,本文中只关注两类字段,一类用于报文数据管理,一类用于 sk_buff 管理。

(1)报文数据管理

sk_buff 中有 4 个指针分别指向数据缓存的不同位置,数据缓存就是实际存放报文数据的一段内存。

head: 数据缓存的起始位置

end: 数据缓存的结束位置

data: 数据存放的起始位置

tail: 数据存放的结束位置

也就是说head和end指向缓存的起止位置,缓存申请好之后,这两个指针指向的位置就保持不变;data和tail指向实际数据存储的起止位置,随着报文在不同的层次之间传递,会出现添加协议头(发送方向,tcp, ip, mac 头逐层添加)或者删除报文头(接收方向,mac, ip, tcp 头逐层删除)的情况,这个时候通过操作data来实现,当需要向缓存中追加数据的时候需要移动tail。headroom是head 和data之间的空间,tailroom是tail和end之间的空间,这两个room的大小 >= 0。

另外还有3个成员 transport_header, network_header, mac_header,分别表示 tcp 头,ip 头, mac 头相对于 head 的偏移量。这 3 个成员是每层协议头相对于 head 的偏移量,不是指针。

从上图中可以看到,sk_buff->end 是报文数据可用空间的结尾,但不是内存 buffer 的结尾,内存 buffer 的结尾处还有一个数据结构 struct skb_shared_info。struct skb_shared_info 两个重要的成员是 frags 和 frag_list。frags 是用在 SG, 网卡支持这种方式,才能向这个里边放数据;frag_list 是 ip 分片。

(2)sk_buff 管理

tcp有发送缓冲区,也有接收缓冲区,缓冲区中的元素是一个skb,缓冲区是一个双向链表;tcp接收侧有一个乱序队列,tcp解决乱序问题主要使用这个队列,这个队列使用红黑树实现。

以 tc 发送缓冲区为例,tcp_sendmsg 中调用tcp_sendmsg_locked, tcp_sendmsg_locked中调用skb_entail将skb入队到发送缓冲区中。tcp发送缓冲保存在struct sock 中,成员struct sk_buff_head sk_write_queue。

下图是 tcp 发送缓冲区的示意图:

tcp接收缓冲区在 struct sock 中,成员是sk_receive_queue, 在函数tcp_queue_rcv中入队。

接收时乱序队列的入队函数是tcp_data_queue_ofo,乱序队列在数据结构struct tcp_sock 中,成员是 out_of_order_queue。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【全栈实战】基于 Vue3 + Wot Design Uni 动手封装组件

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f60a;好久没有更新有关前端实战教程了&#xff0c;本文主要讲解【全栈实战】基于 Vue3 Wot Design Uni 动手封装组件&#xff01; &#x1f60a;这个教程你将会学到技术正确的选型、…

Ajax中的axios

既然提到Ajax&#xff0c;那就先来说一说什么是Ajax吧 关于Ajax Ajax的定义 Asynchronous JavaScript And XML&#xff1a;异步的JavaScript和XML。 反正就是一句话总结&#xff1a; 使用XML HttpRequest 对象与服务器进行通讯。 AJAX 是一种在无需重新加载整个网页的情况下&…

60.基于SSM的个人网站的设计与实现(项目 + 论文)

项目介绍 本站是一个B/S模式系统&#xff0c;个人网站是在MySQL中建立数据表保存信息&#xff0c;运用SSMVue框架和Java语言编写。并按照软件设计开发流程进行设计实现充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SSM的网…

聊聊Flink:Flink的状态管理

一、Flink的状态是什么&#xff1f; 我们知道&#xff0c;Flink的一个算子可能会有多个子任务&#xff0c;每个子任务可能分布在不同的实例&#xff08;即slot&#xff09;上&#xff0c;我们可以把Flink的状态理解为某个算子的子任务在其当前实例上的一个变量&#xff0c;该变…

Idea 将多个module显示在同一个project

Idea 将多个maven项目显示在同一个project下 1、选择菜单 File-》New -》Module from Existing Sources -> 2、在弹出的界面选中对应的Module的pom.xml,然后点击OK按钮就行了 (弹出框上面也提示了Eclipse 项目选中.project文件&#xff1b;Maven 项目选中pom.xml; ) 最终显…

文件解析漏洞中间件(iis和Apache)

IIS解析漏洞 IIS6.X #环境 Windows Server 2003 在iis6.x中&#xff0c;.asp文件夹中的任意文件都会被当做asp文件去执行 在默认网站里创建一个a.asp文件夹并创建一个1.jpg写进我们的asp代码 <%now()%> #asp一句话 <%eval request("h")%> 单独创建一…

gitee别人仓库再上传自己仓库

一、新建一个自己的Git仓库 如果没有注册账号的朋友&#xff0c;可以先去注册一个Gitee的账号&#xff0c;用于管理自己的代码特别好用&#xff01;&#xff01;&#xff01; 接下来就是在gitee上新建一个自己的仓库&#xff0c;如下图所示 二、右建 Git Bush Here删除.git文件…

STM32F407 | Embedded IDE01 - vscode搭建Embedded IDE开发环境(支持JLINK、STLINK、DAPLINK)

导言 Embedded IDE官网:https://em-ide.com/docs/intro 我猜肯定有部分人使用SI Keil开发STM32项目&#xff0c;也有vscode Keil开发STM32程序。SI或vscode编写代码&#xff0c;然后切换Keil编译、下载、调试程序。有一段时间&#xff0c;我也是这么干的。但是&#xff0c;程…

算法的学习笔记—扑克牌顺子(牛客JZ61)

&#x1f600;前言 扑克牌顺子问题是一道趣味性与逻辑性兼备的题目&#xff0c;要求判断五张牌是否能组成顺子&#xff0c;其中大小王&#xff08;癞子&#xff09;可作为任意牌面。癞子的特殊性增加了问题的复杂度&#xff0c;也为解题提供了更多的可能性。通过这一问题&#…

记录遇到的一个新的变种JS加密

源 逻辑分析 混淆代码的目的是隐藏实际逻辑&#xff0c;增加逆向工程的难度。以下是对代码的逐步分析和解读。 第一部分&#xff1a;立即调用的函数表达式 (IIFE) (function () {var _K [...]; // 存储大量字符串的数组 })();​ 1. 目的&#xff1a;这个 IIFE 是整个代码运行…

LeetCode:104.二叉树的最大深度

跟着carl学算法&#xff0c;本系列博客仅做个人记录&#xff0c;建议大家都去看carl本人的博客&#xff0c;写的真的很好的&#xff01; 代码随想录 LeetCode&#xff1a;104.二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节…

Fastjson <= 1.2.47 反序列化漏洞复现

0x01 前言 Fastjson 是一个 Java 语言编写的高性能功能完善的 JSON 库&#xff0c;可以将 Java 对象转换为 JSON 格式&#xff0c;也可以将 JSON 字符串转换为 Java 对象&#xff0c;在中国和美国使用较为广泛。 0x02 漏洞成因 Fastjson < 1.2.68 版本在处理反序列化对象时…

python:函数

一、嵌套函数 1.1概念 嵌套函数是定义在另一个函数作用域内部的函数。外部函数可以访问其内部声明的嵌套函数&#xff0c;而嵌套函数则可以访问其外部函数的作用域&#xff08;包括参数和局部变量&#xff09;。 1.2实例 一般情况下&#xff0c;我们是这样书写嵌套函数的&a…

Linux 下的 GPT 和 MBR 分区表详解

文章目录 Linux 下的 GPT 和 MBR 分区表详解一、分区表的作用二、MBR&#xff08;Master Boot Record&#xff09;1. **特点**2. **优点**3. **缺点**4. **适用场景** 三、GPT&#xff08;GUID Partition Table&#xff09;1. **特点**2. **优点**3. **缺点**4. **适用场景** 四…

基于单片机的智能婴儿床监护系统多功能婴儿床摇篮系统

功能介绍 以STM32单片机为控制核心蓝牙传输控制可以进行哭闹检测、尿床检测、音乐播放、语音提醒、哭闹时可以进行摇床有不同的模式自动模式和睡眠模式 实物可做&#xff0c;其他功能也可以 电路图 PCB 源代码 u8 Temperature_High; //室内温度高阈值 u8 Temperature_…

人工智能在VR展览中扮演什么角色?

人工智能&#xff08;AI&#xff09;在VR展览中扮演着多重关键角色&#xff0c;这些角色不仅增强了用户体验&#xff0c;还为展览的组织者提供了强大的工具。 接下来&#xff0c;由专业从事VR展览制作的圆桌3D云展厅平台为大家介绍AI在VR展览中的一些主要作用&#xff1a; 个性…

JVM和数据库面试知识点

JVM内存结构 主要有几部分&#xff1a;堆、栈、方法区和程序计数器 堆是JVM中最大的一块内存区域&#xff0c;用于存储对象实例&#xff0c;一般通过new创建的对象都存放在堆中。堆被所有的线程共享&#xff0c;但是它的访问时线程不安全的&#xff0c;通常通过锁的机制来保证线…

flask-admin+Flask-WTF 实现实现增删改查

背景&#xff1a; flask-adminflask-wtf在网上可以搜索到很多资料&#xff0c;但有价值的很少&#xff0c;或许是太简单&#xff0c;或者是很少人这么用&#xff0c;或者。。。&#xff0c;本文将作者近礼拜摸索到的一点经验分享出来&#xff0c;给自己做个记录。 材料&#…

C++简明教程(文章要求学过一点C语言)(3)

一、编程工具大揭秘——IDE 当我们准备踏入 C 编程的奇妙世界时&#xff0c;首先要认识一个重要的“魔法盒子”——集成开发环境&#xff08;IDE&#xff09;。IDE 就像是一个全能的编程工作室&#xff0c;它把我们写代码所需要的各种工具都整合到了一起&#xff0c;让编程这件…

STM32-笔记5-按键点灯(中断方法)

1、复制03-流水灯项目&#xff0c;重命名06-按键点灯&#xff08;中断法&#xff09; 在\Drivers\BSP目录下创建一个文件夹exti&#xff0c;在该文件夹下&#xff0c;创建两个文件exti.c和exti.h文件&#xff0c;并且把这两个文件加载到项目中&#xff0c;打开项目工程文件 加载…