深入理解Linux网络笔记(二):内核和用户进程协作之阻塞方式

news2024/11/27 12:56:17

本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动默认采用的都是Intel的igb网卡驱动

Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source

2、内核是如何与用户进程协作的(一)

1)、socket的直接创建

从开发者的角度来看,调用socket函数可以创建一个socket

等这个socket函数调用执行完之后,用户层面看到返回的是一个整数型的句柄,但其实内核在内部创建了一系列socket相关的内核对象。它们互相之间的关系如下图所示:

// net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
	...
	retval = sock_create(family, type, protocol, &sock);
	...
}

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

// net/socket.c
int __sock_create(struct net *net, int family, int type, int protocol,
			 struct socket **res, int kern)
{
	int err;
	struct socket *sock;
	const struct net_proto_family *pf;
	...
  // 分配socket对象
	sock = sock_alloc();
	...
  // 获得每个协议族的操作表  
	pf = rcu_dereference(net_families[family]);
	...
  // 调用指定协议族的创建函数,对于AF_INET对应的是inet_create
	err = pf->create(net, sock, protocol, kern);
	...
}

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

// net/ipv4/af_inet.c
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,
	},
	...
};

static int inet_create(struct net *net, struct socket *sock, int protocol,
		       int kern)
{
	struct sock *sk;
	struct inet_protosw *answer;
	struct inet_sock *inet;
	struct proto *answer_prot;  
	...
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

		err = 0;
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		} else {
			if (IPPROTO_IP == protocol) {
				protocol = answer->protocol;
				break;
			}
			if (IPPROTO_IP == answer->protocol)
				break;
		}
		err = -EPROTONOSUPPORT;
	}
	...
  // 将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);
	...
	// 对sock对象进行初始化
	sock_init_data(sock, sk);
	...
}

在inet_create中,根据类型SOCK_STREAM查找到对于TCP定义的操作方法实现集合inet_stream_ops和tcp_prot,并把它们分别设置到socket->ops和sock->sk_prot上,如下图所示:

再往下看到了sock_init_data。在这个方法中将socket中的sk_data_ready函数指针进行了初始化,设置为默认sock_def_readable,如下图所示:

// 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_def_readable())来唤醒在socket上等待的进程。这个将在后面讲到”软中断模块“时再说

至此,一个tcp对象,确切的说是AF_INET协议族下SOCKET_STREAM对象就算创建完成了。这里花费了一次socket系统调用的开销

2)、内核和用户进程协作之阻塞方式

阻塞IO模型:

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态

在同步阻塞IO模型中,先是用户进程发起创建socket的指令,然后切换到内核态完成了内核对象的初始化。接下来,Linux在数据包的接收上,是硬中断和ksoftirqd线程在进行处理。当ksoftirqd线程处理完以后,再通知相关的用户进程

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

1)等待接收消息

clib库recv函数会执行recvform系统调用。进入系统调用后,用户进程就进入了内核态,执行一系列的内核协议层函数,然后到socket对象的接收队列中查看是否有数据,没有的话就把是自己添加到socket对应的等待队列里。最后让出CPU,操作系统会选择下一个就绪状态的进程来执行。整个流程如下图所示:

接下来根据源码来看更具体的细节。其中要关注的重点是recvfrom最后是怎么把自己的进程阻塞掉的

// net/socket.c
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 = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	err = sock_recvmsg(sock, &msg, size, flags);
	...
}

接下来的调用顺序为:sock_recvmsg => __sock_recvmsg => __sock_recvmsg_nosec

// net/socket.c
static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock,
				       struct msghdr *msg, size_t size, int flags)
{
	...
	return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}

调用socket对象ops里的recvmsg,recvmsg指向的是inet_recvmsg方法

// net/ipv4/af_inet.c
int inet_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		 size_t size, int flags)
{
	...
	err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
	...
}

这里又遇到一个函数指针,这次调用的是socket对象里sk_prot下的recvmsg方法,recvmsg方法对应的是tcp_recvmsg方法

// net/ipv4/tcp.c
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	...
	int copied = 0;
	...
	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);
		...
	} while (len > 0);
	...
}

终于看到了我们想要看的内容,skb_queue_walk在访问sock对象下的接收队列,如下图所示:

如果没有收到数据,或者收到的不够多,则调用sk_wait_data把当前进程阻塞掉

// net/core/sock.c
int sk_wait_data(struct sock *sk, long *timeo)
{
	int rc;
	// 当前进程(current)关联到所定义的等待队列项上
	DEFINE_WAIT(wait);

 	// 调用sk_sleep获取sock对象下的wait
  // 并准备挂起,将当前进程设置为可打断(INTERRUPTIBLE)
	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));
	...
}

下面再来详细看看sk_wait_data是怎样把当前进程给阻塞掉的,如下图所示:

首先在DEFINE_WAIT宏下,定义了一个等待队列项wait。在这个新的等待队列项上,注册了回调函数autoremove_wake_function,并把当前进程描述符current关联到其.private成员上

// include/linux/wait.h
#define DEFINE_WAIT_FUNC(name, function)				\
	wait_queue_t name = {						\
		.private	= current,				\
		.func		= function,				\
		.task_list	= LIST_HEAD_INIT((name).task_list),	\
	}

#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

紧接着在sk_wait_data中调用sk_sleep获取socket对象下的等待队列列表头wait_queue_head_t。sk_sleep源码如下:

// include/net/sock.h
static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
	BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
	return &rcu_dereference_raw(sk->sk_wq)->wait;
}

接着调用prepare_to_wait来把新定义的等待队列项wait插入sock对象的等待队列

// kernel/wait.c
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	if (list_empty(&wait->task_list))
		__add_wait_queue(q, wait);
	set_current_state(state);
	spin_unlock_irqrestore(&q->lock, flags);
}

这样后面当内核收完数据产生就绪事件的时候,就可以查找socket等待队列上的等待项,进而可以找到回调函数和在等待该socket就绪事件的进程了

最后调用sk_wait_event让出CPU,进程将进入睡眠状态,这会产生一次进程上下文切换的开销,这个开销是昂贵的,大约需要消耗几个微妙的CPU时间

2)软中断模块

前文讲到了网络包到网卡后是怎么被网卡接收,最后再交由软中断处理的,这里直接从TCP协议的接收函数tcp_v4_rcv看起,总体接收流程如下图所示:

软中断(也就是Linux里的ksoftirqd线程)里收到数据以后,发现是TCP包就会执行tcp_v4_rcv函数。接着往下,如果是ESTABLISH状态下的数据包,则最后会把数据拆出来放到对应socket的接收队列中,然后调用sk_data_ready来唤醒用户进程

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
	...
	// 获取tcp header
	th = tcp_hdr(skb);
	// 获取ip header
	iph = ip_hdr(skb);
	...
	// 根据数据包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);
		}
	}
	...
}

在tcp_v4_rcv中,首先根据收到的网络包的header里的source和dest信息在本机上查询对应的socket。找到以后,调用tcp_v4_do_rcv函数

// net/ipv4/tcp_ipv4.c
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;
	}
	// 其他非ESTABLISH状态的数据包处理
	...
}

假设处理的是ESTABLISH状态下的包,这样就进入tcp_rcv_established函数进行处理

// net/ipv4/tcp_input.c
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);
			...
}

在tcp_rcv_established中通过调用tcp_queue_rcv函数,完成了将接收到的数据放到socket的接收队列上,如下图所示:

函数tcp_queue_rcv的源码如下:

// net/ipv4/tcp_input.c
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
		  bool *fragstolen)
{
	...
  // 把接收到的数据放到socket的接收队列的尾部  
	if (!eaten) {
		__skb_queue_tail(&sk->sk_receive_queue, skb);
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}

调用tcp_queue_rcv接收完成之后,接着调用sk_data_ready来唤醒在socket上等待的用户进程。这又是一个函数指针。在前面”socket的直接创建“的部分,讲到在创建socket的流程里执行到的sock_init_data函数已经把sk_data_ready指针设置成了sock_def_readable函数了。它是默认的数据就绪处理函数

// net/core/sock.c
static void sock_def_readable(struct sock *sk, int len)
{
	struct socket_wq *wq;

	rcu_read_lock();
	wq = rcu_dereference(sk->sk_wq);
  // 有进程在此socket的等待队列
	if (wq_has_sleeper(wq))
    // 唤醒等待队列上的进程
		wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
						POLLRDNORM | POLLRDBAND);
	sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
	rcu_read_unlock();
}

在sock_def_readable中再一次访问到了sock->sk_wq下的wait。在前面”等待接收消息“的部分调用recvform时,在执行过程的最后,通过DEFINE_WAIT(wait)将当前进程关联的等待队列添加到sock->sk_wq下的wait里了

那接下来就是调用wake_up_interruptible_sync_poll来唤醒在socket上因为等待数据而被阻塞掉的进程了,如下图所示:

// include/linux/wait.h
#define wake_up_interruptible_sync_poll(x, m)				\
	__wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
// kernel/sched/core.c
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;
	int wake_flags = WF_SYNC;

	if (unlikely(!q))
		return;

	if (unlikely(!nr_exclusive))
		wake_flags = 0;

	spin_lock_irqsave(&q->lock, flags);
	__wake_up_common(q, mode, nr_exclusive, wake_flags, key);
	spin_unlock_irqrestore(&q->lock, flags);
}

__wake_up_common实现唤醒。该函数调用的参数nr_exclusive传入的是1,这里指的是即使有多个进程都阻塞在同一个socket上,也只会唤醒一个进程。其作用是为了避免惊群,而不是把所有的进程都唤醒

// kernel/sched/core.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
		unsigned flags = curr->flags;

		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}

__wake_up_common中找出一个等待队列项curr,然后调用其curr->func。在前面”等待接收消息“的部分recv函数执行的时候,使用DEFINE_WAIT()定义等待队列项时,内核把curr->func设置成了autoremove_wake_function

// kernel/wait.c
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	int ret = default_wake_function(wait, mode, sync, key);

	if (ret)
		list_del_init(&wait->task_list);
	return ret;
}

在autoremove_wake_function中,调用了default_wake_function

// kernel/sched/core.c
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
			  void *key)
{
	return try_to_wake_up(curr->private, mode, wake_flags);
}

调用try_to_wake_up时传入的task_struct是curr->private,这个就是因为等待而被阻塞的进程项。当这个函数执行完的时候,在socket上等待而被阻塞的进程就被推入可运行队列里了,这又将产生一次进程上下文切换的开销

3)同步阻塞总结

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

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

同步阻塞总体流程如下图所示:

每次一个进程专门为了等一个socket上的数据就被从CPU上拿下来,然后换上另一个进程,如下图所示。等到数据准备好,睡眠的进程又被唤醒,总共产生两次进程上下文切换开销

推荐阅读:

Linux五种I/O模型:带你彻底理解Linux五种I/O模型

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

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

相关文章

疯 狂 的 文 件 夹 【收藏起来】

文章目录 📁故事的开始📁一起疯狂📚资源领取 专栏Python零基础入门篇🔥Python网络蜘蛛🔥Python数据分析Django基础入门宝典🔥小玩意儿🔥Web前端学习tkinter学习笔记Excel自动化处理 &#x1f4…

力扣-345.反转字符串中的元音字母

Idea 将s中的元音字母存在字符串sv中,并且使用一个数组依次存储元音字母的下标。 然后将字符串sv进行反转,并遍历元音下标数组,将反转后的字符串sv依次插入到源字符串s中 AC Code class Solution { public:string reverseVowels(string s) {…

力扣 -- 1049. 最后一块石头的重量 II(01背包问题)

参考代码&#xff1a; 未优化代码&#xff1a; class Solution { public:int lastStoneWeightII(vector<int>& stones) {int nstones.size();int sum0;for(const auto& e:stones){sume;}int aimsum/2;//多开一行&#xff0c;多开一列vector<vector<int&g…

排序---P1116 车厢重组

P1116 车厢重组 来自 <车厢重组 - 洛谷> 其实这道题本质上就是求逆序对的过程&#xff1a; 两种方法&#xff1a;一个是通过冒泡排序过程求逆序对&#xff1b;一个是通过归并排序过程求逆序对。 法一&#xff1a;当通过冒泡排序进行正序排列时&#xff0c;相邻两个数需要…

批量将文件名称符合要求的文件自动复制到新文件夹:Python实现

本文介绍基于Python语言&#xff0c;读取一个文件夹&#xff0c;并将其中每一个子文件夹内符合名称要求的文件加以筛选&#xff0c;并将筛选得到的文件复制到另一个目标文件夹中的方法。 本文的需求是&#xff1a;现在有一个大的文件夹&#xff0c;其中含有多个子文件夹&#x…

Redis与分布式-集群搭建

接上文 Redis与分布式-哨兵模式 1. 集群搭建 搭建简单的redis集群&#xff0c;创建6个配置&#xff0c;开启集群模式&#xff0c;将之前配置过的redis删除&#xff0c;重新复制6份 针对主节点redis 1&#xff0c;redis 2&#xff0c;redis 3都是以上修改内容&#xff0c;只是…

C++位图—布隆过滤器

目录 位图概念位图应用 布隆过滤器简介布隆过滤器的优缺点布隆过滤器应用场景布隆过滤器实现布隆过滤器误判率分析 总结 位图概念 位图是一种数据结构&#xff0c;用于表示一组元素的存在或不存在&#xff0c;通常用于大规模数据集的快速查询。它基于一个位数组&#xff08;或位…

管理经济学基本概念(二): 规模经济、需求曲线、供给曲线等

1、关键术语 1.1、边际报酬递减规律 边际报酬递减规律是指随着产出量的扩大&#xff0c;边际生产率(与增量投入要素相联系的增量产出量)最终会下降。 递增的边际生产率意味着边际成本递增。 递增的边际成本最终导致平均成本递增。 1.2、规模经济 (1) 如果长期平均成本相对…

打开泰坦陨落2找不到msvcp120.dll无法执行代码/msvcr120.dll丢失修复方法

msvcp120.dll 是 Windows 操作系统中的一个动态链接库文件&#xff0c;对于许多程序和游戏的运行起着至关重要的作用。然而&#xff0c;有时候我们可能会遇到 msvcp120.dll 丢失的情况&#xff0c;导致电脑出现各种问题。本文将详细介绍 msvcp120.dll 丢失的四种解决方法&#…

【项目】5.1阻塞和非阻塞、同步和异步 5.2Unix、Linux上的五种IO模型

5.1阻塞和非阻塞、同步和异步&#xff08;网络IO&#xff09; 典型的一次IO的两个阶段是什么&#xff1f;数据就绪和数据读写 数据就绪&#xff1a;根据IO操作的就绪状态 阻塞非阻塞 数据读写&#xff1a;根据应用程序和内核的交互方式 同步异步 陈硕&#xff1a;在处理IO的…

【小沐学Python】各种Web服务器汇总(Python、Node.js、PHP、httpd、Nginx)

文章目录 1、Web服务器2、Python2.1 简介2.2 安装2.3 使用2.3.1 http.server&#xff08;命令&#xff09;2.3.2 socketserver2.3.3 flask2.3.4 fastapi 3、NodeJS3.1 简介3.2 安装3.3 使用3.3.1 http-server&#xff08;命令&#xff09;3.3.2 http3.3.3 express 4、PHP4.1 简…

选择排序算法:简单但有效的排序方法

在计算机科学中&#xff0c;排序算法是基础且重要的主题之一。选择排序&#xff08;Selection Sort&#xff09;是其中一个简单但非常有用的排序算法。本文将详细介绍选择排序的原理和步骤&#xff0c;并提供Java语言的实现示例。 选择排序的原理 选择排序的核心思想是不断地从…

网络工程师怎么才算开窍

做网络工程师怎么样才算开窍&#xff1f;刚才有个朋友他说他希望从事网络工程师之后若干年。比如说当他到35岁的时候&#xff0c;他不希望出现跟今天现在是2023年&#xff0c;今年的这种裁员潮里面所遇到的那些主人公&#xff0c;就是技术学的也不错&#xff0c;然后工作也不错…

37 二叉树的最大深度

二叉树的最大深度 题解1 深度优先搜索&#xff08;递归弹栈&#xff09;题解2 广度优先搜索&#xff08;队列&#xff09; 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 提示&#xff1a; 树中节点…

阿里云PolarDB自研数据库详细介绍_兼容MySQL、PostgreSQL和Oracle语法

阿里云PolarDB数据库是阿里巴巴自研的关系型分布式云原生数据库&#xff0c;PolarDB兼容三种数据库引擎&#xff1a;MySQL、PostgreSQL、Oracle&#xff08;语法兼容&#xff09;&#xff0c;目前提供云原生数据库PolarDB MySQL版、云原生数据库PolarDB PostgreSQL版和云原生数…

使用 Python 的多项 Logistic 回归问题

一、说明 多项逻辑回归是一种统计方法&#xff0c;用于预测两个以上类别的分类结果。当因变量是分类变量而不是连续变量时&#xff0c;它特别有用。 二、分类预测 在多项式逻辑回归中&#xff0c;模型预测属于因变量每个类别的观测值的概率。这些概率可以解释为观察结果属于每…

聊聊并发编程——原子操作类和Fork/Join框架

目录 原子操作类 实现原子性原理 保证原子性的方法 Fork/Join框架 分而治之 工作窃取算法 Fork/Join框架的设计 示例 原子操作类 线程A和线程B同时更新变量i进行操作i1,最后的结果可能i不等于3而是等于2。这是线程不安全的更新操作&#xff0c;一般我们会使用Synchron…

CCF CSP认证 历年题目自练Day18

CCF CSP认证 历年题目自练Day18 题目一 试题编号&#xff1a; 201809-1 试题名称&#xff1a; 卖菜 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 256.0MB 问题描述&#xff1a; 问题描述   在一条街上有n个卖菜的商店&#xff0c;按1至n的顺序排成一排&#xff0c;这…

如何保持终身学习

文章目录 2.1. 了解你的大脑2.2 学习是对神经元网络的塑造2.3 大脑的一生 3.学习的心里基础3.1 固定思维与成长思维3.2 我们为什么要学习 4. 学习路径4.1 构建知识模块4.2 大脑是如何使用注意力的4.3 提高专注力4.4 放松一下&#xff0c;学的更好4.5 巩固你的学习痕迹4.6 被动学…

amazon自养号测评:为卖家提供稳定转化率的解决方案

亚马逊作为全球最大的跨境电商平台之一&#xff0c;吸引了大量卖家进入市场。然而&#xff0c;如何提高产品的转化率&#xff0c;吸引更多买家并促使他们下单&#xff0c;对卖家来说仍然是一个关键问题。本文将分享一些亚马逊卖家可以采用的小技巧&#xff0c;帮助他们实现这一…