深入理解Linux内核网络(二):内核与用户进程的协作

内核在协议栈接收处理完输入包以后,要能通知到用户进程,让用户进程能够收到并处理这些数据。进程和内核配合有很多种方案,第一种是同步阻塞的方案,第二种是多路复用方案。本文以epoll为例

部分内容来源于 《深入理解Linux网络》、《Linux内核源码分析TCP实现》

socket

在网络编程中,套接字(Socket)是用于描述计算机网络中通信端点的抽象概念。它允许应用程序在网络上进行数据传输,通过特定的 API 与底层协议(如 TCP 或 UDP)交互。套接字可以分为流式套接字(用于 TCP 连接,提供可靠的字节流通信)和数据报套接字(用于 UDP 连接,提供无连接的不可靠通信)。常见操作包括创建套接字、绑定地址、监听连接、接受和发送数据等。

int socket(int domain, int type, int protocol);

在这里插入图片描述
创建完socket之后,内核其实在内部创建了一系列的对象,部分对象如上所示。

创建流程

接下来解析socket的创建流程以及其大体包含哪些内容:

首先是socket系统调用,调用__sys_socket,其中首先调用sock_create创建socket。

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    return __sys_socket(family, type, protocol);
}

/**
    family: 常用AF_INET(ipv4),AF_UNIX(本地),AF_INET6(ipv6)
    type: SOCK_STREAM,SOCK_DGRAM,SOCK_RAW
*/
int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    // 创建socket
    retval = sock_create(family, type, protocol, &sock);

    // 将socket和文件描述符关联,并返回对应描述符
    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

初始化socket

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

int __sock_create(struct net *net, int family, int type, int protocol,
             struct socket **res, int kern)
{
    struct socket *sock;
    const struct net_proto_family *pf;

    sock = sock_alloc();
    // 获取协议族
    pf = rcu_dereference(net_families[family]);
    // 调用协议族的create函数
    pf->create(net, sock, protocol, kern);
    ...
}

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

// file: net/ipv4/af_inet.c

static struct inet_protosw inetsw_array[] =
{   
    // 可以看到tcp对应的type和protocol
    {
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .flags =      INET_PROTOSW_PERMANENT |
                  INET_PROTOSW_ICSK,
    },
}

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

 /* look for the requested type/protocol pair. */
lookup_protocol:

    /**
        每个type都有一个链表,里面对应不同协议的对象
        例如:type为SOCK_DGRAM的链表,protocol包含UDP,ICMP等
    */
    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
        // 更据protocol匹配,得到anser
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else
        ......
    }
    
    // 将 inet_stream_ops 赋到 socket->ops 上
    sock->ops = answer->ops;
    
    // 获得 tcp_prot
    answer_prot = answer->prot;
    
    // 分配 sock 对象,并把 tcp_prot 赋到 sock->sk_prot 上
    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);

    // 初始化,且sock->sk = sk;
    sock_init_data(sock, sk);

}

inet _create中,根据类型SOCK_STREAM查找到对于TCP定义的操作方法实现集合inet_stream _opstcp_prot,并把它们分别设置到socket- >opssock->sk_prot上,如图下图所示。

在这里插入图片描述

再往下看到了sock_init_data。在这个方法中将sock中的sk _data ready函数指针进行了初始化,设置为默认sock_def_readable

// file: net/core/sock.c

void sock_init_data(struct socket *sock, struct sock *sk) {
    sk->sk_data_ready = sock_def_readable;
    sk->sk_write_space = sock_def_write_space;
    sk->sk_error_report = sock_def_error_report;
}

当软中断上收到数据包时会通过调用sk_data_ready函数指针来唤醒在sock上等待的进程。至此,一个tcp对象就算创建完成了,这里花费了一次socket系统调用的开销。

与sockfs关联

创建完成后将socket和文件描述符关联,并返回对应描述符。关于socket和文件描述符关联,实际上Linux中存在sockfs的虚拟文件系统专门用来管理套接字,会创建并关联对应inode,将socket视为文件管理。

// file: net/socket.c

/**
    创建sock时实际sockfs创建inode,和socket关联
 */
struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;

    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    sock = SOCKET_I(inode);

    inode->i_ino = get_next_ino();
    inode->i_mode = S_IFSOCK | S_IRWXUGO;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();
    inode->i_op = &sockfs_inode_ops;

    return sock;
}


static int sock_map_fd(struct socket *sock, int flags)
{
    struct file *newfile;
    // 从当前进程files表中获取未使用fd
    int fd = get_unused_fd_flags(flags);

    //通过sockfs创建一个于传入socket关联的file
    newfile = sock_alloc_file(sock, flags, NULL);

    if (!IS_ERR(newfile)) {
    // 将fd与socket对应file关联
        fd_install(fd, newfile);
        return fd;
    }
}

/**
    为socket创建file,并关联
*/
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
    struct file *file;

    if (!dname)
        dname = sock->sk ? sock->sk->sk_prot_creator->name : "";

    file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
                O_RDWR | (flags & O_NONBLOCK),
                &socket_file_ops);

    // sock和file互相关联
    sock->file = file;
    file->private_data = sock;
    return file;
}

小结

socket套接字创建流程如下:

  • 系统调用:用户程序通过调用 socket() 系统调用请求创建一个新的socket。

  • 进入内核态:系统调用被转发到内核中的处理函数。

  • 分配socket结构:内核分配一个 struct socket 结构体,用于描述该socket的状态和属性。

  • 分配sock结构:分配一个 struct sock 结构体,表示与网络协议相关的信息。

  • 初始化socket数据:调用 sock_init_data() 函数初始化 struct sock 中的各种回调函数。

  • 协议族和协议的注册:根据传入的协议族和类型选择合适的协议。在TCP/IP协议栈中,调用 inet_create() 或类似函数来创建对应的协议对象。

  • 绑定sockfs:将刚创建的socket结构体与sockfs绑定。sockfs是Linux内核中用于处理套接字的虚拟文件系统。这个过程通常在 socket 创建后完成,使得该socket可以被视为文件描述符。在这个阶段,内核会设置socket的操作和协议相关的功能,以便为后续的操作(如bind()、connect()等)做好准备。

  • 分配端口和地址:如果是流式socket(如TCP),内核将分配一个本地端口和地址,以便后续的连接请求。

  • 返回socket描述符:一旦所有的初始化工作完成,内核会返回一个文件描述符给用户程序,表示新创建的socket。

  • 后续操作:用户可以使用返回的socket描述符进行后续的操作,例如绑定地址 (bind())、监听连接 (listen()) 和接受连接 (accept()) 等。

同步阻塞I/O

从用户进程创建socket,到一个网络包抵达网卡被用户进程接收,同步阻塞IO总体上的流程如图所示。

在这里插入图片描述

等待接收消息

ssize_t recv(int sockfd, void buf[.len], size_t len,int flags);
ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
                        int flags,
                        struct sockaddr *_Nullable restrict src_addr,
                        socklen_t *_Nullable restrict addrlen);

recv会执行recvform系统调用。进入系统调用后,用户进程就进入了内核态,执行一系列的内核协议层函数,然后到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)
{
    return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len);
}

int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
           struct sockaddr __user *addr, int __user *addr_len)
{
    struct socket *sock;
    struct iovec iov;
    struct msghdr msg;
    struct sockaddr_storage address;

    // 将用户空间缓冲区转换为内核空间可以使用的iovec
    err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);

    // 通过fd获取socket对象
    sock = sockfd_lookup_light(fd, &err, &fput_needed);

    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    ...

    // 接收数据并通过msg.msg_iter写入用户缓冲区
    err = sock_recvmsg(sock, &msg, flags);

    //addr不为NULL时获取对等方地址
    if (err >= 0 && addr != NULL) {
        move_addr_to_user(&address,msg.msg_namelen, addr, addr_len);
    }

}

通过查找当前进程文件表可以获取fd对应的file对象,之前讲过file和socket关联,从而获取对应socket。在sock_recvmsg函数中会从socket中获取数据并写入用户缓冲区。

static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
                     int flags)
{
    return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
                  inet_recvmsg, sock, msg, msg_data_left(msg),
                  flags);
}

const struct proto_ops inet_stream_ops = {
    .recvmsg       = inet_recvmsg,
};

最终会调用到sock_recvmsg_nosec函数,其中又会调用sock->ops->recvmsg,在这里即调用inet_recvmsg。而在inet_recvmsg中又会调用sock成员的函数sk->sk_prot->recvmsg,即tcp_recvmsg

在这里插入图片描述

int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
         int flags)
{
    struct sock *sk = sock->sk;
    err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg,
                  sk, msg, size, flags & MSG_DONTWAIT,
                  flags & ~MSG_DONTWAIT, &addr_len);
}

struct proto tcp_prot = {
    .recvmsg        = tcp_recvmsg,
    ...
};

tcp_recvmsg函数会从sock结构体的接收队列中获取skbuff并拷贝数据到用户缓冲区。

//file: net/ipv4/tcp.c

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
        int flags, int *addr_len)
{
    do {
            // 在循环中不断从接收队列获取数据
            last = skb_peek_tail(&sk->sk_receive_queue);
            skb_queue_walk(&sk->sk_receive_queue, skb) {
                last = skb;
                offset = *seq - TCP_SKB_CB(skb)->seq;
                if (offset < skb->len)
                    goto found_ok_skb;
                if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
                    goto found_fin_ok;
                ...
            }
            ...
            if (copied >= target) {
                /* Do not sleep, just process backlog. */
                release_sock(sk);
                lock_sock(sk);
            } else {
                // 没有拷贝足够数据,等待,阻塞当前进程
                sk_wait_data(sk, &timeo, last);
            }

            found_ok_skb:
                // 将skbuff内核态数据拷贝到用户缓冲区
                used = skb->len - offset;
                skb_copy_datagram_msg(skb, offset, msg, used);
        } while (len > 0)
}

在这里插入图片描述
skb_queue_walk函数在读取sock对象下的接收队列,如果数据不够多则调用sk_wait_data

sk_wait_data函数会阻塞进程,其内部如下:

// file:net/core/sock.c

int sk_wait_data(struct sock *sk, long *timeo, const struct sk_buff *skb)
{
    DEFINE_WAIT_FUNC(wait, woken_wake_function);
    int rc;

    // 向sock等待队列添加等待项
    add_wait_queue(sk_sleep(sk), &wait);
    sk_set_bit(SOCKWQ_ASYNC_WAITDATA, sk);
    // 等待条件为接收队列尾部元素改变,陷入阻塞
    rc = sk_wait_event(sk, timeo, skb_peek_tail(&sk->sk_receive_queue) != skb, &wait);
    sk_clear_bit(SOCKWQ_ASYNC_WAITDATA, sk);
    remove_wait_queue(sk_sleep(sk), &wait);
    return rc;
}

在这里插入图片描述
其将当前进程的等待项添加到与 socket (sk) 相关联的等待队列中。sk_sleep(sk) 返回一个指向与该 socket 相关的睡眠队列的指针。之后设置 socket 的状态位,以指示当前进程正在等待数据。

之后调用 sk_wait_event,该函数会检查条件表达式(即接收队列尾部元素是否改变)并可能导致阻塞。如果条件不满足,进程将进入休眠状态,直到有其他进程唤醒它。
timeo 参数可以指定超时值,在指定时间内如果条件仍未满足,进程将被唤醒。

当有数据到达 socket 或者其他条件发生变化时,其他进程会调用相应的唤醒函数,如 wake_up(),从而将这个等待队列中的进程唤醒。在退出之前,函数会清除设置的状态位,并从等待队列中移除当前进程的等待项。

整个过程涉及一次进程上下文转换。

软中断模块唤醒进程

前文讲到了网络包到网卡后是怎么被网卡接收,最后再交由软中断处理的,这里直接从TCP协议的接收函数tcp _v4_rcv看起。

在这里插入图片描述
软中断(也就是Linux里的ksoftirqd线程)里收到数据包以后,发现是TCP包就会执行tcp_v4_rcv函数。

int tcp_v4_rcv通过IP和端口获取对应的struct sock对象,进一步调用tcp_v4_do_rcv,主要看其中对ESTABLISHED状态下的数据处理——tcp_rcv_established,在其中会进行TCP协议的相关处理,之后将处理完成的sk_buff加入sock对象的接收队列中,然后执行sk->sk_data_ready(sk)。在socket创建部分我们知道该函数指针指向sock_def_readable函数,在其中获取sock对象的等待队列,唤醒等待的进程。

// file:net/ipv/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    th = (const struct tcphdr *)skb->data; // 获取tcp header
    iph = ip_hdr(skb);  // 获取ip header

    // 根据数据包的ip,端口信息找到对应struct sock
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);

    ...
    if (!sock_owned_by_user(sk)) {
        // 调用tcp_v4_do_rcv进一步处理
        ret = tcp_v4_do_rcv(sk, skb);
    }
}
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    struct sock *rsk;

    // ESTABLISHED状态下的数据处理
    if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
        tcp_rcv_established(sk, skb);
        return 0;
    }
}

// file: net/ipve/tcp_input.c
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    // 一系列处理
    ......
    // 将处理好的sk_buff放入sock对象的接收队列
    tcp_queue_rcv(sk, skb, &fragstolen);

    // 出发就绪事件
    tcp_data_ready(sk);
}

void tcp_data_ready(struct sock *sk)
{
    ...
    sk->sk_data_ready(sk);
}

// file: net/core/sock.c
static void sock_def_readable(struct sock *sk)
{
    struct socket_wq *wq;

    rcu_read_lock();
    wq = rcu_dereference(sk->sk_wq);
    if (skwq_has_sleeper(wq))
        wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
                        EPOLLRDNORM | EPOLLRDBAND);
    sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
    rcu_read_unlock();
}

sock_def_readable函数中wake_up_interruptible_sync_poll宏的内容如下,其中nr_exclusive参数传入1,代表即使多个进程阻塞在同一个sock上也只唤醒一个进程,避免“惊群”。

#define wake_up_interruptible_sync_poll(x, m)                   \
    __wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, poll_to_key(m))

void __wake_up_sync_key(struct wait_queue_head *wq_head, unsigned int mode,
            int nr_exclusive, void *key) {}

小结

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

第一部分是自己的代码所在的进程,调用的socket()函数会进入内核态创建必要内核对象。recv()函数在进入内核态以后负责查看接收队列,以及在没有数据可处理的时候把当前进程阻塞掉,让出CPU。

第二部分是硬中断、软中断上下文(系统线程ksoftirqd)。在这些组件中,将包处理完后会放到socket的接收队列中。然后根据socket内核对象找到其等待队列中正在因为等待而被阻塞掉的进程,把它唤醒。

异步阻塞

在Linux上多路复用方案有select、poll、epoll。它们三个中的epoll的性能表现是最优秀的,能支持的并发量也最大。所以把epoll作为要拆解的对象,深入揭秘内核是如何实现多路的IO管理的。

epoll解析

小结

epoll的数据结构:

  • rb_root rbr,这是红黑树的根节点,存储着所有添加到 epoll 中的事件,也就是这个 epoll 监控的事件。
  • list_head rdllist 这是一个双向链表,保存着将要通过 epoll_wait 返回给用户的、满足条件的事件。

epoll的操作: 调用 epoll_create 建立一个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源)、调用 epoll_ctl 向 epoll 对象中添加连接的套接字、调用 epoll_wait 收集发生事件的连接。

当进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象,也就是应用程序中的 epfd(epoll 文件描述符) 所代表的对象。eventpoll 对象也是文件系统中的一员,和socket一样也有一个等待队列。

创建epoll对象 eventpoll 之后,可以使用 epoll_ctl 添加或者删除所要监听的socket。内核会将eventpoll添加到需要监听的socket的等待队列中。当socket收到数据后,中断回调程序会操作eventpoll对象,而不是直接操作进程。

在 eventpoll 对象中存在就绪列表,rdlist(双向链表保存着将要通过 epoll_wait 返回给用户满足条件的事件)。中断回调程序会给eventpoll的就绪列表添加socket的引用。eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。

epoll_wait的返回条件也是根据rdlist的状态进行判断:如果rdlist已经引用了socket,那么epoll_wait直接返回(把发生的事件的集合从内核复制到 events数组中);如果rdlist为空,阻塞进程。

(对于epoll,操作系统只需要将进程放入eventpoll这一个对象的等待队列中;而对于select,操作系统则需要将进程放入到socket列表中的所有socket对象的等待队列中。)

疑问点

多路复用epoll为什么就能提高网络性能?

epoll高性能最根本的原因是极大程度地减少了无用的进程上下文切换,让进程更专注地处理网络请求。

在内核的硬、软中断上下文中,包从网卡接收过来进行处理,然后放到socket的接收队列。再找到socket关联的epitem,并把它添加到epoll对象的就绪链表中。

在用户进程中,通过调用epoll_wait来查看就绪链表中是否有事件到达,如果有,直接取走进行处理。处理完毕再次调用epoll_wait。在高并发的实践中,只要连接足够多,epoll_wait根本不会让进程阻塞。用户进程会一直处理,直到epoll_wait里实在没活儿可干的时候才主动让出CPU。这是epoll高效的核心原因所在。

红黑树仅仅是提高了epoll查找、添加、删除socket时的效率而已,不算epoll在高并发场景高性能的根本原因。

epoll也是阻塞的?

很多人以为只要一提到阻塞,就是性能差,其实这就冤枉了阻塞。阻塞说的是进程因为等待某个事件而主动让出CPU挂起的操作。

例如,一个epoll对象下添加了一万个客户端连接的socket。假设所有这些socket上都还没有数据达到,这个时候进程调用epoll_wait发现没有任何事情可干。这种情况下用户进程就会被阻塞掉,而这种情况是完全正常的,没有工作需要处理,那还占着CPU是没有道理的。

阻塞不会导致低性能,过多过频繁的阻塞才会。epoll的阻塞和它的高性能并不冲突。

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

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

相关文章

101. 对称二叉树【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路3.1 递归3.2 迭代 四、参考代码4.1 递归4.2 迭代 零、原题链接 101. 对称二叉树 一、题目描述 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 进阶&#xff1a;你可以运用递归和迭代两种方法解决…

【MySQL】使用 JDBC 连接数据库

文章目录 前言1. 认识 JDBC1.1 概念1.2 好处 2. 使用 JDBC2.1 安装数据驱动包2.2 把 jar 包导入到项目中2.3 代码编写2.4 测试结果 3. 代码优化4. 源码展示结语 前言 在 MySQL 系列中&#xff0c;我们介绍了很多内容&#xff0c;包括但不限于建库建表&#xff0c;增删查改等等…

微信步数C++

题目&#xff1a; 样例解释&#xff1a; 【样例 #1 解释】 从 (1,1) 出发将走 2 步&#xff0c;从 (1,2) 出发将走 4 步&#xff0c;从 (1,3) 出发将走 4 步。 从 (2,1) 出发将走 2 步&#xff0c;从 (2,2) 出发将走 3 步&#xff0c;从 (2,3) 出发将走 3 步。 从 (3,1) 出发将…

基于基于微信小程序的社区订餐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

ElasticSearch备考 -- Async search

一、题目 通过异步方式查询earthquakes索引下Magnitude大于5的数据 二、思考 正常的查询大家可能会用的多一点&#xff0c;这种异步查询为数据量比较大的查询在后台执行&#xff0c;不用同步等待结果&#xff0c;待执行完成在获取结果。 三、解题 Step 1、准备基础数据 # D…

Sping源码:三级缓存

目录 一、概念1、三级缓存的作用2、循环依赖的含义 二、代码1、代码下载2、文件功能介绍3、源码分析3.1、找到获取A对象的位置&#xff0c;打断点进行debug操作3.2、一步步找到在A对象中注入B对象的位置3.3、一步步找到B对象注入A对象的位置3.4、往下找到通过三级缓存解决循环依…

YouTube音视频合并批处理基于 FFmpeg的

专门针对YouTube高品质分享处理的&#xff0c;将音频和视频合并。 首先下载ffmpeg.exe网上随便下载。 echo off title YouTube 音视频合并 20241004 echo 作者&#xff1a;xiaoshen echo 网站&#xff1a;http://www.xiaoshen.cn/ echo. set /p audio请将【音频】文件拖拽到此…

六、Java 基础语法(下)

一、变量 1、变量的定义与使用 变量就是内存中的存储空间&#xff0c;空间中存储着经常发生改变的数据变量定义格式&#xff1a; 数据类型 变量名 数据值使用时根据变量名使用举例如下&#xff0c;上面是代码&#xff0c;下面是输出 2、变量的注意事项 变量名不允许重复…

Vue入门-指令学习-v-show和v-if

v-show&#xff1a; 作用&#xff1a;控制元素的显示隐藏 语法&#xff1a;v-show"表达式" 表达式值true显示&#xff0c;false隐藏 v-if 作用&#xff1a;控制元素的显示隐藏&#xff08;条件渲染&#xff09; 语法&#xff1a; vif"表达式" 表达式tr…

字节跳动收购Oladance耳机:强化音频技术,加速VR/AR生态布局

字节跳动收购Oladance耳机&#xff1a;加码VR/AR领域布局 近日&#xff0c;字节跳动宣布已完成对开放式耳机品牌Oladance的收购&#xff0c;实现了对该品牌的100%控股。这一收购标志着字节跳动在AI硬件领域的进一步扩展和深化&#xff0c;特别是对其VR/AR领域布局的重要加码。 …

STM32使用Keil5 在运行过程中不复位进入调试模式

一、选择Options for Target进入设置 二、选择所使用的调试器&#xff0c;这里以ST-Link为例。取消勾选Load Application at Startup 可以在进入调试模式的时候不会从新加载程序&#xff01;从而不破坏现场 三、点击Setting进入 四、取消勾选Reset after Connect 使得调试器连接…

DotNetty ChannelRead接收数据为null

问题&#xff1a;C#使用Dotnetty和Java netty服务器通讯&#xff0c;结果能正确发送数据到服务器&#xff0c;却始终接收不到服务器返回的数据。 解决&#xff1a;一定一定要注意服务器和客户端使用的编码一定要完全一样才行 我先前在客户端添加了StringDecoder,服务器却没有…

malloc源码分析之 ----- 你想要啥chunk

文章目录 malloc源码分析之 ----- 你想要啥chunktcachefastbinsmall binunsorted binbin处理top malloc源码分析之 ----- 你想要啥chunk tcache malloc源码&#xff0c;这里以glibc-2.29为例&#xff1a; void * __libc_malloc (size_t bytes) {mstate ar_ptr;void *victim;vo…

Windows安装Linux子系统报错:WslRegisterDistribution failed with error: 0x8007019e

WslRegisterDistribution failed with error: 0x8007019e 报错截图如下图&#xff1a; 该处是由于没有安装Linux内核&#xff0c;因此需要安装。可前往官网查看详情&#xff1a;https://aka.ms/wslinstall 需要解决该问题&#xff0c;可参照官网方法&#xff08;我没试过官网…

【优选算法之队列+宽搜/优先级队列】No.14--- 经典队列+宽搜/优先级队列算法

文章目录 前言一、队列宽搜示例&#xff1a;1.1 N 叉树的层序遍历1.2 ⼆叉树的锯⻮形层序遍历1.3 ⼆叉树最⼤宽度1.4 在每个树⾏中找最⼤值 二、优先级队列&#xff08;堆&#xff09;示例&#xff1a;2.1 最后⼀块⽯头的重量2.2 数据流中的第 K ⼤元素2.3 前 K 个⾼频单词2.4 …

Android车载——VehicleHal初始化(Android 11)

1 概述 VehicleHal是AOSP中车辆服务相关的hal层服务。它主要定义了与汽车硬件交互的标准化接口和属性管理&#xff0c;是一个独立的进程。 2 进程启动 VehicleHal相关代码在源码树中的hardware/interfaces/automotive目录下 首先看下Android.bp文件&#xff1a; cc_binary …

Maven的生命周期与依赖作用域介绍

说明&#xff1a;本文介绍Maven的生命周期&#xff0c;以及在pom.xml文件中每个依赖&#xff08;dependency标签内&#xff09;scope标签的内容。 Maven生命周期 在IDEA项目中&#xff0c;右侧边栏&#xff0c;点Maven&#xff0c;可以看到以下生命周期。 其中&#xff0c; c…

Spring MVC 常用注解

目录 基础概念 常用注解介绍 基础概念 1、MVC &#xff1a;代表一种软件架构设计思想&#xff0c;通俗的理解&#xff1a;客户端发送请求到后台服务器的Controller(C)&#xff0c;控制器调用Model(M)来处理业务逻辑&#xff0c;处理完成后&#xff0c;返回处理后的数据到Vie…

Deformable Transformer论文笔记

原文链接 [2010.04159] Deformable DETR: Deformable Transformers for End-to-End Object Detection (arxiv.org)https://arxiv.org/abs/2010.04159 原文笔记 What 作者结合了可变形卷积的稀疏空间采样和 Transformer 的关系建模能力的优点。提出了Deformable Detr Defor…

文心一言 VS 讯飞星火 VS chatgpt (359)-- 算法导论24.3 1题

一、在图 24-2上运行Dijkstra算法&#xff0c;第一次使用结点 s s s作为源结点&#xff0c;第二次使用结点 z z z作为源结点。以类似于图 24-6 的风格&#xff0c;给出每次while循环后的 d d d值和 π π π值&#xff0c;以及集合 S S S中的所有结点。如果要写代码&#xff0c…