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

news2024/11/30 4:54:47

内核在协议栈接收处理完输入包以后,要能通知到用户进程,让用户进程能够收到并处理这些数据。进程和内核配合有很多种方案,第一种是同步阻塞的方案,第二种是多路复用方案。本文以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的阻塞和它的高性能并不冲突。

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

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

相关文章

认知杂谈72《别让梦想只是梦!7步跃过现实高墙的终极攻略!》

内容摘要&#xff1a;         梦想的实现是一场与现实的较量&#xff0c;需要坚持和突破。学习路线图对于掌握技能至关重要&#xff0c;如学编程应从基础语法开始&#xff0c;逐步深入。 面对难题&#xff0c;积极搜索、提问和实践是关键。坚持和专注是成功的核心&#…

《Windows PE》4.1.3 IAT函数地址表

IAT&#xff08;Import Address Table&#xff09;表又称为函数地址表&#xff0c;是Windows可执行文件中的一个重要数据结构&#xff0c;用于存储导入函数的实际入口地址。 在可执行文件中&#xff0c;当一个模块需要调用另一个模块中的函数时&#xff0c;通常会使用导入函数…

十、敌人锁定

方法&#xff1a;通过寻找最近的敌人&#xff0c;使玩家的面朝向始终朝向敌人&#xff0c;进行攻击 1、代码 在这个方法中使用的是局部变量&#xff0c;作为临时声明和引用 public void SetActorAttackRotation() {Enemys GameObject.FindGameObjectsWithTag("Enemy&qu…

机器学习-树模型算法

机器学习-树模型算法 一、Bagging1.1 RF1.2 ET 二、Boosting2.1 GBDT2.2 XGB2.3 LGBM 仅个人笔记使用&#xff0c;感谢点赞关注 一、Bagging 1.1 RF 1.2 ET 二、Boosting 2.1 GBDT 2.2 XGB 2.3 LGBM LightGBM&#xff08;Light Gradient Boosting Machine) 基本算法原理…

2024企业网盘排行榜,十大企业网盘深度评测【part 2】

在当今数字化时代&#xff0c;企业网盘已成为提升工作效率、保障数据安全的重要工具。从Box到腾讯企业网盘&#xff0c;再到Egnyte、Amazon Drive、金山文档&#xff08;WPS&#xff09;和Huddle&#xff0c;每款产品都有其独特的功能和应用场景。然而&#xff0c;在众多选择中…

Spring Boot新闻推荐:实时数据处理

4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式&#xff0c;是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示&#xff1a; 图4-1系统工作原理…

简单二叉树的构建及遍历

1.主要函数 #include <stdio.h> #include <stdlib.h> #include <string.h>//创建节点结构体 typedef struct node{char data[16];//节点数据struct node *L;//左节点struct node *R;//右结点}tree,*treeptr;//先序方式创建节点 treeptr create() {char buf[…

idea创建springboot模块

1.点击file->新建->model server url&#xff1a;如果倒数第二个java选项没有11&#xff0c;就把这里改为阿里云的 name&#xff1a;模块名字 location&#xff1a;文件存放的位置 其他的根据图片自行填写 2. 3.验证 如果没有iml文件(不影响&#xff0c;可以不弄)&#…

MongoDB聚合操作及索引底层原理

目录 链接:https://note.youdao.com/ynoteshare/index.html?id=50fdb657a9b06950fa255a82555b44a6&type=note&_time=1727951783296 本节课的内容: 聚合操作: 聚合管道操作: ​编辑 $match 进行文档筛选 ​编辑 将筛选和投影结合使用: ​编辑 多条件匹配: …

20241004给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Android12时永不休眠的步骤

20241004给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Android12时永不休眠的步骤 2024/10/4 19:22 1、 Z:\rk3588s4_3588a12\device\rockchip\common\device.mk ifeq ($(strip $(BOARD_HAVE_BLUETOOTH_RTK)), true) include hardware/realtek/rtkbt/rtkbt.mk endif ifeq ($(str…

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

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

⌈ 传知代码 ⌋ 将一致性正则化用于弱监督学习

&#x1f49b;前情提要&#x1f49b; 本文是传知代码平台中的相关前沿知识与技术的分享~ 接下来我们即将进入一个全新的空间&#xff0c;对技术有一个全新的视角~ 本文所涉及所有资源均在传知代码平台可获取 以下的内容一定会让你对AI 赋能时代有一个颠覆性的认识哦&#x…

什么是 NVIDIA 机密计算?( 上篇 )

什么是机密计算? 文章目录 前言1. 机密计算定义2. 机密计算有何独特之处?3. 机密计算是如何得名的4. 机密计算的工作原理是什么?5. 缩小安全边界6. 机密计算的使用案例7. 机密计算如何发展8. 加速机密计算9. 机密计算的下一步前言 机密计算是一种在计算机处理器的受保护区域…

全网最详细kubernetes中的资源

1、资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管理kubernetes。 kubernetes的本质上就是一个集群系统&#xff0c;用户可以在集群中部署各种服务。 所谓的部署服务&#xff0c;其实就是在kubernetes集群中运行一个个的…

csp-j模拟三补题报告

前言 今天题难&#xff0c;排名没进前十 &#xff08;“关于二进制中一的个数的研究与规律”这篇文章正在写&#xff09; 第一题 三个&#xff08;three&#xff09; 我的代码&#xff08;AC&#xff09; #include<bits/stdc.h> #define ll long long using namespac…

快停止这种使用U盘的行为!

前言 现在各行各业的小伙伴基本上都需要用电脑来办公了&#xff0c;你敢说你不需要用电脑办公&#xff1f; 啊哈哈哈&#xff0c;用iPad或者手机办公的也算。 有些小伙伴可能经常996&#xff0c;甚至有时候都是007。有时候到了下班时间&#xff0c;工作还没做完&#xff0c;…

Python技巧:如何处理未完成的函数

一、问题的提出 写代码的时候&#xff0c;我们有时候会给某些未完成的函数预留一个空位&#xff0c;等以后有时间再写具体内容。通常&#xff0c;大家会用 pass 或者 ... &#xff08;省略号&#xff09;来占位。这种方法虽然能让代码暂时不报错&#xff0c;但可能在调试的时候…

精准翻译神器:英汉互译软件的卓越表现

英文作为目前世界上使用最广的一种语言&#xff0c;是的很多先进的科学文献或者一些大厂产品的说明书都有英文的版本。为了方便我们的阅读和学习&#xff0c;现在有不少支持翻译英汉互译的工具&#xff0c;今天我们就一起来讨论一下吧。 1.福昕中英在线翻译 链接直达>>…

二叉树的前序遍历——非递归版本

1.题目解析 题目来源&#xff1a;144.二叉树的前序遍历——力扣 测试用例 2.算法原理 前序遍历&#xff1a; 按照根节点->左子树->右子树的顺序遍历二叉树 二叉树的前序遍历递归版本十分简单&#xff0c;但是如果树的深度很深会有栈溢出的风险&#xff0c;这里的非递归…

【论文笔记】DKTNet: Dual-Key Transformer Network for small object detection

【引用格式】&#xff1a;Xu S, Gu J, Hua Y, et al. Dktnet: dual-key transformer network for small object detection[J]. Neurocomputing, 2023, 525: 29-41. 【网址】&#xff1a;https://cczuyiliu.github.io/pdf/DKTNet%20Dual-Key%20Transformer%20Network%20for%20s…