linux之网络子系统-用户层接收数据包之同步阻塞方案

news2024/12/23 9:48:56

一、前言

之前讲述了网络包是如何从网卡送到协议栈的,接下来内核还有一项重要的工作,就是在协议栈接收处理完输入包后要通知到用户进程,如何用户进程接收到并处理这些数据。

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

  1. 同步阻塞方案(Java中习惯叫BIO)
  2. 多路IO复用方案(Java中对应NIO)
    1. Linux多路复用有select、poll、epoll,这里只讲性能最优秀的epoll

本文主要讲的是同步阻塞模式的实现方案,多路IO复用方案及问题解答在下一篇文章讲解。

二、相关实际问题

  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. 1k_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更为精细的状态,其可能的取值如下:
    1. 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; 
      }
      
  3. 这些取值从名字上看,似乎只使用于TCP协议,但事实上,UDP和RAW也借用了其中一些值,在一个socket创建之初,其取值都是TCP_CLOSE,一个UDP socket connect完成后,将这个值改为TCP_ESTABLISHED,最后,关闭sockt前置回TCP_CLOSE,RAW也一样。
  4. sk_rcvbuf和sk_sndbuf:表示接收和发送缓冲区的大小。这两个值是动态的,应用程序可以通过setsockopt系统调用来改变它们的值。但是,这些值也受到了一些全局内核参数的限制(通常由/proc/sys/net/core/rmem_default(对于接收缓冲区)和/proc/sys/net/core/wmem_default(对于发送缓冲区)这两个内核参数来决定)。
  5. sk_receive_queue和sk_write_queue:接收缓冲队列和发送缓冲队列,队列里排列的是套接字缓冲区struct sk_buff,队列中的struct sk_buff的字节数总和不能超过缓冲区大小的设定。在sock实例创建的时候初始化的,最开始为空的队列(双向链表)。
  6. 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为空队列。

当软中断上收到数据包时会通过调用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;
    ......
    
    // 如果设置了MSG_WAITALL,则target==len,即recv函数中的参数len
    // 如果没设置MSG_WAITALL,则target==1
    target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
    
    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的接收队列,如果接收到的数据不满足目标数量则会阻塞当前进程,具体阻塞方法的实现逻辑如下:

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);
	}
    } else
	// 如果数据包被用户进程锁定,则数据包进入后备处理队列,并且该进程进入
	// 套接字的后备处理等待队列sk->lock.wq
	sk_add_backlog(sk, skb);
}

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

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

找到以后,首先要判断socket是否已经被用户锁定

在Linux中,如果一个 sock 对象被用户进程锁定(例如,用户进程正在调用 recv 等系统调用读取数据),那么内核就不应该直接修改 sock 的状态,而应该将接收到的数据包放入后备处理队列,稍后再处理(当数据包被添加到后备处理队列后,这个数据包的处理就结束了,只有当 socket 变为空闲状态,那些在后备处理队列中的数据包才会被处理,通常这个处理过程就包括将数据包添加到接收队列中)。

如果socket没有被锁定,则调用tcp_prequeue尝试将数据包添加到sock的预处理队列中。如果添加成功则返回1,否则返回0。

在这个函数里面,会对 sysctl_tcp_low_latency 进行判断,也即是不是要低时延地处理网络包。如果把 sysctl_tcp_low_latency 设置为 0,那就要放在 prequeue 队列中暂存,这样不用等待网络包处理完毕,就可以离开软中断的处理过程,但是会造成比较长的时延(因为数据包的处理延迟到了进程被调度的时候。对于接收数据包而言没有区别,因为都是需要到进程被调度时才拷贝到用户空间。但是由于TCP协议处理被延迟,导致ACK的发送延迟,从而使数据发送端的数据发送延迟)。如果把 sysctl_tcp_low_latency 设置为 1,则调用 tcp_v4_do_rcv()立即处理。

际上代码中较新的版本已经没有了tcp_prequeue()函数。之所以取消prequeue,是因为在大多使用事件驱动(epoll)的当下,已经很少有阻塞在recvfrom()或者read()的服务端代码了

如果数据包没有加入预处理队列则进入接收的主体函数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)同步队列阻塞总结

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

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

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

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

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

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

所以我们需要更高效的网络IO模型!多路复用方案(epoll)继续学习多路IO复用解决方案~

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

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

相关文章

高效消防应急:RFID技术救援装备的快速管理

基层应急救援消防设施管理面临着一个既复杂又迫切的挑战。通常,受限的资源和专业人才的短缺导致应对突发事件的反应迟缓,处理结果不理想。同时,消防团队的人员结构和技术能力也在一定程度上决定了应急救援的成效和效率。在数字化浪潮下&#…

TMDOG的Gin学习笔记_01——初识Gin框架

TMDOG的Gin学习笔记_01——初识Gin框架 博客地址:[TMDOG的博客](https://blog.tmdog114514.icu) 作者自述: 停更太久了,是因为开学了课太多了,并且我一直在准备上篇文章的内容正在coding,就先搁置了更新博客QAQ&…

【ROS的TF系统】

系列文章目录 TF系统简介 前面的章节实现了SLAM节点的建图功能: 激光雷达节点—> /scan话题 —>hector_mapping节点—> 地图数据话题/map 本期来实现SLAM节点的定位功能: TF(TransForm)主要描述的是两个坐标系的空间关…

Pandas JSON学习

1.JSON简介 JSON(JavaScript Object Notation,JavaScript 对象表示法),是存储和交换文本信息的语法,类似 XML。JSON 比 XML 更小、更快,更易解析,Pandas 可以很方便的处理 JSON 数据。 [{"…

SQL Server身份验证模式

SQL Server是一个广泛使用的关系数据库管理系统,通常使用两种身份验证模式:Windows身份验证和SQL Server身份验证。理解这些身份验证方式的概念与更改方式的操作,对于数据库管理员和开发者至关重要。本文将详细介绍身份验证方式的概念以及如何…

DC-9靶机通关

这是这个系列的最后一个靶机了!!!经过前面的锻炼和学习,这次我的目标是尽量不借助任何教程或者提示来拿下这个靶机!!!下面我们看能不能成功!!! 1.实验环境 攻…

百度SEO分析实用指南 提升网站搜索排名的有效策略

内容概要 在数字化时代,搜索引擎优化(SEO)已经成为提升网站曝光度的关键工具。本指南将带您了解SEO的基本知识,帮助您在复杂的网络环境中立足。我们将从关键词优化开始,重点讲解如何选择合适的关键词来提高搜索引擎排…

ML2001-1 机器学习/深度学习 Introduction of Machine / Deep Learning

图片说明来自李宏毅老师视频的学习笔记,如有侵权,请通知下架 影片参考 【李宏毅】3.第一节 - (上) - 机器学习基本概念简介_哔哩哔哩_bilibili 1. 机器学习的概念与任务类型 概念:机器学习近似于寻找函数,用于处理不同类型的任…

用Python打造媒体管理播放器:从零到全功能GUI应用

背景 在日常生活中,我们经常需要管理和播放大量媒体文件。市面上的音频播放器可能功能单一,或者界面复杂。作为一名程序员,我决定使用Python自己打造一个简单yet强大的媒体管理播放器。 C:\pythoncode\new\playsong.py 全部代码 import os…

Cisco Packet Tracer 8.0 路由器静态路由配置

文章目录 静态路由简介一、定义与特点二、配置与命令三、优点与缺点四、应用场景 一,搭建拓扑图二,配置pc IP地址三,pc0 ping pc1 timeout四,配置路由器Router0五,配置路由器Router1六,测试 静态路由简介 …

【HarmonyOS】鸿蒙系统

文章目录 前言一、鸿蒙OS概述1. 定义与特性2. 核心技术理念3. 技术架构设计1. 应用层2. 框架层3. 系统服务层4. 内核层 二、分布式架构分布式架构的核心理念分布式能力的实现关键技术 三、 总结 前言 鸿蒙OS是由华为推出的一款开源操作系统,旨在满足智能终端设备的…

《双指针篇》---移动零

题目传送门 这道题可以归类为 数组划分/数组分块 。 题目制定了一个规则,我们可以在这个规则下,将数组划分为若干个区间。 这道题让我们把所有非零元素移动到左边。所有零元素移动到右边。 将数组划分为: 左区间非0; 右区间&…

网络编程项目之UDP聊天室

项目要求 利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。 问题思考 客户端会不会知道其它客户端地址? UDP客户端不会直接互连,所以不会获知其它客户端地址…

【NOIP普及组】 FBI树

【NOIP普及组】 FBI树 C语言版本C 版本Java版本Python版本 💐The Begin💐点点关注,收藏不迷路💐 我们可以把由“0”和“1”组成的字符串分为三类:全“0”串称为B串,全“1”串称为I串,既含“0”又…

Lucene的概述与应用场景(1)

文章目录 第1章 Lucene概述1.1 搜索的实现方案1.1.1 传统实现方案1.1.2 Lucene实现方案 1.2 数据查询方法1.1.1 顺序扫描法1.1.2 倒排索引法 1.3 Lucene相关概念1.3.1 文档对象1.3.2 域对象1)分词2)索引3)存储 1.3.3 常用的Field种类 1.4 分词…

不适合的学习方法

文章目录 不适合的学习方法1. 纯粹死记硬背2. 过度依赖单一资料3. 线性学习4. 被动学习5. 一次性学习6. 忽视实践7. 缺乏目标导向8. 过度依赖技术9. 忽视个人学习风格10. 过于频繁的切换 结论 以下是关于不适合的学习方法的更详细描述,包括额外的内容和相关公式&…

华为OD机试真题(Python/JS/C/C++)- 考点 - 细节

华为OD机试 2024E卷题库疯狂收录中,刷题 点这里。 本专栏收录于《华为OD机试真题(Python/JS/C/C)》。

Linux中使用NGINX

NGINX简介 Nginx(engine x)是俄罗斯人编写的十分轻量级的HTTP服务器是一个高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/SMTP代理服务器官方网站:http://nginx.org/ NGINX概述 Nginx默认配置文件:/etc/ngin…

scrapy爬取名人名言

爬取名人名言:http://quotes.toscrape.com/ 1 创建爬虫项目,在终端中输入: scrapy startproject quotes2 创建之后,在spiders文件夹下面创建爬虫文件quotes.py,内容如下: import scrapy from scrapy.spi…

mmsegmentation训练自己的数据集

文章目录 前言一、安装MMSegmentation二、数据集转换1.labelme标签数据转化为voc数据 三、训练设置1.建立数据集文件,并存入数据集2.设置训练配置文件 四、使用官方权重1、选择预测的方法2、查看方法支持的预训练数据集和权重权重位置找到对应的数据集下载权重 3、使…