网络-内核是如何与用户进程交互

news2024/9/20 14:07:58

1、socket的直接创建

在这里插入图片描述

net/socket.c

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

int sock_create(int family, int type, int protocol, struct socket **res)
{
	return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

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,这个函数在inet_init函数中被初始化
	err = pf->create(net, sock, protocol, kern);
	...
}
EXPORT_SYMBOL(__sock_create);

socket在内核中是怎么创建的?
sock_create->__sock_create->inet_create
在__sock_create里首先调用sock_alloc来分配一个struct sock内核对象,接着获取协议簇的操作函数表,调用其create方法。对于AF_INET协议簇来说,执行到的是inet_create函数。

static int inet_create(struct net *net, struct socket *sock, int protocol,
                       int kern)
{
	struct sock *sk;            // 指向 sock 结构体,表示套接字
	struct inet_protosw *answer; // 指向 inet_protosw 结构体,表示协议开关
	struct inet_sock *inet;     // 指向 inet_sock 结构体,表示 IPv4 套接字
	struct proto *answer_prot;  // 指向 proto 结构体,表示协议操作
	unsigned char answer_flags;  // 协议开关标志
	int try_loading_module = 0;  // 尝试加载模块的次数
	int err;                    // 用于存储函数返回值

	/*
	 * 检查协议号是否在有效范围内。
	 * 如果不在,返回 -EINVAL 错误。
	 */
	if (protocol < 0 || protocol >= IPPROTO_MAX)
		return -EINVAL;

	/*
	 * 初始化套接字状态为未连接。
	 */
	sock->state = SS_UNCONNECTED;

	/*
	 * 查找请求的类型/协议对。
	 * 首先尝试在 RCU 读锁保护下查找协议。
	 */
lookup_protocol:
	err = -ESOCKTNOSUPPORT; // 设置错误为“不支持的套接字类型”
	rcu_read_lock();        // 锁定 RCU 读锁

	/*
	 * 遍历协议开关列表,查找匹配的协议。
	 * 如果找到匹配的协议,跳出循环。
	 */
	list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
		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; // 设置错误为“不支持的协议”
	}

	/*
	 * 如果没有找到匹配的协议,尝试加载相应的模块。
	 * 如果模块加载后,再次尝试查找协议。
	 */
	if (unlikely(err)) {
		if (try_loading_module < 2) {
			rcu_read_unlock(); // 解锁 RCU 读锁
			if (++try_loading_module == 1)
				request_module("net-pf-%d-proto-%d-type-%d",
					       PF_INET, protocol, sock->type);
			else
				request_module("net-pf-%d-proto-%d",
					       PF_INET, protocol);
			goto lookup_protocol; // 重新查找协议
		} else {
			goto out_rcu_unlock; // 如果模块加载失败,跳转到错误处理
		}
	}

	/*
	 * 检查是否允许非内核进程创建原始套接字。
	 * 如果不允许,返回 -EPERM 错误。
	 */
	err = -EPERM;
	if (sock->type == SOCK_RAW && !kern &&
	    !ns_capable(net->user_ns, CAP_NET_RAW))
		goto out_rcu_unlock;

	/*
	 * 设置套接字的操作函数和协议。
	 */
	sock->ops = answer->ops;
	//获得tcp_port
	answer_prot = answer->prot;
	answer_flags = answer->flags;
	rcu_read_unlock(); // 解锁 RCU 读锁

	/*
	 * 分配套接字内存。
	 * 如果分配失败,返回 -ENOBUFS 错误。
	 */
	err = -ENOBUFS;
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
	if (!sk)
		goto out;

	/*
	 * 初始化套接字。
	 */
	err = 0;
	if (INET_PROTOSW_REUSE & answer_flags)
		sk->sk_reuse = SK_CAN_REUSE; // 设置套接字重用标志

	inet = inet_sk(sk); // 获取 IPv4 套接字结构体
	inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0; // 设置是否为控制套接字

	// 初始化其他 IPv4 特定字段
	inet->nodefrag = 0;
	if (SOCK_RAW == sock->type) {
		inet->inet_num = protocol;
		if (IPPROTO_RAW == protocol)
			inet->hdrincl = 1;
	}
	if (net->ipv4.sysctl_ip_no_pmtu_disc)
		inet->pmtudisc = IP_PMTUDISC_DONT;
	else
		inet->pmtudisc = IP_PMTUDISC_WANT;
	inet->inet_id = 0;

	// 关联套接字和协议
	sock_init_data(sock, sk);

	sk->sk_destruct	   = inet_sock_destruct;
	sk->sk_protocol	   = protocol;
	sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;

	inet->uc_ttl	= -1;
	inet->mc_loop	= 1;
	inet->mc_ttl	= 1;
	inet->mc_all	= 1;
	inet->mc_index	= 0;
	inet->mc_list	= NULL;
	inet->rcv_tos	= 0;

	sk_refcnt_debug_inc(sk); // 增加套接字引用计数

	// 如果设置了特定协议号,添加到协议哈希链
	if (inet->inet_num) {
		err = sk->sk_prot->hash(sk);
		if (err) {
			sk_common_release(sk);
			goto out;
		}
	}

	// 如果协议需要,调用协议的初始化函数
	if (sk->sk_prot->init) {
		err = sk->sk_prot->init(sk);
		if (err) {
			sk_common_release(sk);
			goto out;
		}
	}

	// 对于非内核进程,运行 BPF cgroup 套接字挂钩程序
	if (!kern) {
		err = BPF_CGROUP_RUN_PROG_INET_SOCK(sk);
		if (err) {
			sk_common_release(sk);
			goto out;
		}
	}
out:
	return err; // 返回结果
out_rcu_unlock:
	rcu_read_unlock(); // 解锁 RCU 读锁
	goto out; // 跳转到函数出口
}

在inet_create中根据类型SOCK_STREAM查找到对于TCP定义的操作方法实现集合inet_stream_ops和tcp_port,并把它们分别设置到socket->ops和sock->sk_port上。
在这里插入图片描述

void sock_init_data(struct socket *sock, struct sock *sk)
{
	/* 初始化套接字通用字段 */
	sk_init_common(sk);
	...
	/* 设置套接字的接收和发送缓冲区大小 */
	sk->sk_rcvbuf = sysctl_rmem_default; // 接收缓冲区默认大小
	sk->sk_sndbuf = sysctl_wmem_default; // 发送缓冲区默认大小
	...
	/* 设置套接字状态为 TCP_CLOSE(关闭状态) */
	sk->sk_state = TCP_CLOSE;

	/* 将套接字与 socket 结构体关联 */
	sk_set_socket(sk, sock);
	/* 设置套接字的回调函数 */
	//当套接字的状态发生变化时,这个回调函数被调用。例如,套接字从监听状态变为已连接状态,或者从已连接状态变为关闭状态。
	sk->sk_state_change = sock_def_wakeup;
	//当套接字接收队列中有数据可读时,这个回调函数被调用。它通知套接字数据已经准备好,可以被用户空间读取。
	sk->sk_data_ready = sock_def_readable;
	//当套接字的发送队列有足够的空间来接受新的数据时,这个回调函数被调用。它通知套接字发送缓冲区不再满,可以发送更多数据。
	sk->sk_write_space = sock_def_write_space;
	//当套接字遇到错误时,这个回调函数被调用。它负责向用户空间报告错误,例如连接重置、数据传输错误等。
	sk->sk_error_report = sock_def_error_report;
	//当套接字被销毁时,这个回调函数被调用。它负责执行清理操作,释放套接字占用的资源。
	sk->sk_destruct = sock_def_destruct;

	/* 设置套接字的接收和发送超时时间 */
	sk->sk_rcvtimeo = MAX_SCHEDULE_TIMEOUT;
	sk->sk_sndtimeo = MAX_SCHEDULE_TIMEOUT;
	...
	/* 设置套接字的时间戳 */
	sk->sk_stamp = SK_DEFAULT_STAMP;

}
EXPORT_SYMBOL(sock_init_data);

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

同步阻塞IO总体流程如下:
在这里插入图片描述

2.1 等待接收消息

recv函数通过strace命令跟踪,可以看到recv会执行recvform调用。
在这里插入图片描述
下面从源代码看看recvfrom是怎么把自己阻塞掉的。

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
		unsigned int, flags, struct sockaddr __user *, addr,
		int __user *, addr_len)
{
	...
	err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
	if (unlikely(err))
		return err;
	
	// 根据用户传入的fd找到socket对象
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	err = sock_recvmsg(sock, &msg, flags);
	...
}

接下来调用顺序为:
sock_recvmsg==>sock_recvmsg_nosec==>inet_recvmsg==>tcp_recvmsg

int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
	int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);

	return err ?: sock_recvmsg_nosec(sock, msg, flags);
}

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

int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
		 int flags)
{
	struct sock *sk = sock->sk;
	int addr_len = 0;
	int err;

	sock_rps_record_flow(sk);

	err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
	if (err >= 0)
		msg->msg_namelen = addr_len;
	return err;
}

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
		int flags, int *addr_len)
{
	...
	do {
		...
		//遍历接收队列接收数据
		skb_queue_walk(&sk->sk_receive_queue, skb) {
			...
		//数据接收完成则返回
		if (copied >= target && !sk->sk_backlog.tail)
			break;

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else {
			//没有收到足够的数据,启用sk_wait_data阻塞当前进程,等待数据到来的通知
			sk_wait_data(sk, &timeo, last);
		}
		...
	} while (len > 0);

}
EXPORT_SYMBOL(tcp_recvmsg);

这里可以看到,skb_queue_Walk在访问sock对象下的接收队列,如果没有收到数据或者收到的不够多,那么调用sk_wait_data将当前进程阻塞。
在这里插入图片描述

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);
	//通过调用wait_woken让出CPU,然后进入睡眠
	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;
}

#define sk_wait_event(__sk, __timeo, __condition, __wait)		\
	({	int __rc;						\
		release_sock(__sk);					\
		__rc = __condition;					\
		if (!__rc) {						\
			*(__timeo) = wait_woken(__wait,			\
						TASK_INTERRUPTIBLE,	\
						*(__timeo));		\
		}							\
		sched_annotate_sleep();					\
		lock_sock(__sk);					\
		__rc = __condition;					\
		__rc;							\
	})

在这里插入图片描述
当内核收完数据产生就绪事件的时候,会通过回调查找socket等待队列项,进而可以找到回调函数和在等待该socket就绪事件的进程。

2.2 软中断模块

网络包从网卡后怎么接收再交给软中断处理的,这篇文章有描述:添加链接描述
下面从tcp_v4_rcv的源码开始看,总体接收流程如下:
在这里插入图片描述
软中断里收到数据之后,发现是TCP包就会执行tcp_v4_rcv函数。如果是ESTABLISH状态下的数据包,则最终会把数据解析出来放到对应socket的接收队列中,然后调用sk_data_ready来唤醒用户进程。

int tcp_v4_rcv(struct sk_buff *skb)
{
	...
	th = (const struct tcphdr *)skb->data;	//获取tcp header
	iph = ip_hdr(skb);	//获取ip header
	...
lookup:
	//根据数据包header中的IP、端口信息查找到对应的socket
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, &refcounted);	
	if (!sk)
		goto no_tcp_socket;

process:
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;

	if (sk->sk_state == TCP_NEW_SYN_RECV) {
		struct request_sock *req = inet_reqsk(sk);
		struct sock *nsk;

		sk = req->rsk_listener;
		if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
			sk_drops_add(sk, skb);
			reqsk_put(req);
			goto discard_it;
		}
		if (unlikely(sk->sk_state != TCP_LISTEN)) {
			inet_csk_reqsk_queue_drop_and_put(sk, req);
			goto lookup;
		}
		/* We own a reference on the listener, increase it again
		 * as we might lose it too soon.
		 */
		sock_hold(sk);
		refcounted = true;
		nsk = tcp_check_req(sk, skb, req, false);
		if (!nsk) {
			reqsk_put(req);
			goto discard_and_relse;
		}
		if (nsk == sk) {
			reqsk_put(req);
		} else if (tcp_child_process(sk, nsk, skb)) {
			tcp_v4_send_reset(nsk, skb);
			goto discard_and_relse;
		} else {
			sock_put(sk);
			return 0;
		}
	}
	if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
		__NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP);
		goto discard_and_relse;
	}

	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;

	if (tcp_v4_inbound_md5_hash(sk, skb))
		goto discard_and_relse;

	nf_reset(skb);

	if (tcp_filter(sk, skb))
		goto discard_and_relse;
	th = (const struct tcphdr *)skb->data;
	iph = ip_hdr(skb);

	skb->dev = NULL;

	if (sk->sk_state == TCP_LISTEN) {
		ret = tcp_v4_do_rcv(sk, skb);
		goto put_and_return;
	}
	//socket未被用户锁定
	if (!sock_owned_by_user(sk)) {
		if (!tcp_prequeue(sk, skb))
			ret = tcp_v4_do_rcv(sk, skb);
	} else if (tcp_add_backlog(sk, skb)) {
		goto discard_and_relse;
	}
}

在tcp_v4_do_rcv中,首先找到对应skb对应的socket,然后进入tcp_v4_do_rcv。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	struct sock *rsk;

	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		struct dst_entry *dst = sk->sk_rx_dst;

		sock_rps_save_rxhash(sk, skb);
		sk_mark_napi_id(sk, skb);
		if (dst) {
			if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
			    !dst->ops->check(dst, 0)) {
				dst_release(dst);
				sk->sk_rx_dst = NULL;
			}
		}
		//执行连接状态下的数据处理
		tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
		return 0;
	}
	//其他非ESTABLISH状态的数据包处理
	...
}

在tcp_v4_do_rcv中会调用tcp_rcv_established,在tcp_rcv_established中会调用tcp_queue_rcv把数据添加到接收队列末尾,然后调用sock的sk_data_ready函数指针,这个指针在sock初始化的时候已经被设置成了sock_def_readable,所以会调用到sock_def_readable。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			 const struct tcphdr *th, unsigned int len)
{
	struct tcp_sock *tp = tcp_sk(sk); // 获取 TCP 套接字的特定结构体
	skb_mstamp_get(&tp->tcp_mstamp); // 获取当前时间戳
	if (unlikely(!sk->sk_rx_dst)) // 检查接收目的地是否已设置
		inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb); // 设置接收目的地

	/*
	 * 头部预测。
	 * 代码大致遵循 Van Jacobson 的 "30 instruction TCP receive"。
	 */
	tp->rx_opt.saw_tstamp = 0; // 初始化接收选项

	/* 检查 TCP 头部的有效性 */
	if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
	    TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
	    !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
		int tcp_header_len = tp->tcp_header_len;
		if (len <= tcp_header_len) {
			...
			if (!eaten) {
				...
				/* 大量数据传输:接收方 */
				eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen); // 将数据包排入接收队列
			}

			tcp_event_data_recv(sk, skb); // 触发数据接收事件

			if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
				/* 处理 ACK */
				tcp_ack(sk, skb, FLAG_DATA);
				tcp_data_snd_check(sk); // 检查发送数据
				if (!inet_csk_ack_scheduled(sk))
					goto no_ack;
			}

			__tcp_ack_snd_check(sk, 0); // 检查 ACK 发送
no_ack:
			if (eaten)
				kfree_skb_partial(skb, fragstolen); // 释放部分数据包
			sk->sk_data_ready(sk); // 通知套接字有数据可读
			return;
		}
	}
	...
}
EXPORT_SYMBOL(tcp_rcv_established);

在这里插入图片描述

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
		  bool *fragstolen)
{
	int eaten;
	struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

	__skb_pull(skb, hdrlen);
	eaten = (tail &&
		 tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
	tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
	//将接收到的数据添加到尾部
	if (!eaten) {
		__skb_queue_tail(&sk->sk_receive_queue, skb);
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}
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, POLLIN | POLLPRI |
						POLLRDNORM | POLLRDBAND);
	sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
	rcu_read_unlock();
}

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

//nr_exclusive被宏定义成了1,是为了防止惊群
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;
	int wake_flags = 1; /* XXX WF_SYNC */

	if (unlikely(!q))
		return;

	if (unlikely(nr_exclusive != 1))
		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);
}

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实现唤醒,该函数调用的传入参数nr_exclusive写死了为1,这里是指即使有多个进程都阻塞在同一个socket上,也只唤醒一个进程。其作用是为了避免“惊群”。在recv注册等待队列项的时候,内核把curr->func设置成了woken_wake_function;在woken_wake_function中调用了woken_wake_function,最终调用到了try_to_wake_up,调用try_to_wake_up传入的参数curr->private就是当时因为等待而被阻塞的进程任务。当这个函数执行完的时候,在socket上等待而被阻塞的进程就被推入可运行队列了。

int woken_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	wait->flags |= WQ_FLAG_WOKEN;

	return default_wake_function(wait, mode, sync, key);
}

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);
}

同步阻塞整体流程图如下:
在这里插入图片描述

文章内容参考:《深入理解Linux网络》
Linux版本用的是4.12,书中用的是3.10

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

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

相关文章

字符串函数的使用与模拟(2)——C语言内存函数

目录 1. memcpy函数的使用与模拟 2. memmove函数的使用与模拟 3. memset函数的使用 4. memcmp函数的使用 5. memchr函数的使用 前言&#xff1a;C语言内存函数是一组用于直接操作计算机内存的内置函数。使用时要包含头文件<string.h> 1. memcpy函数的使用与模拟 函…

【MYSQL表的增删改查(进阶)】

MYSQL表的增删改查&#xff08;进阶&#xff09; 一、新增二、查询2.1 聚合查询2.1.1 聚合函数count&#xff08;&#xff09;sum&#xff08;&#xff09;AVG&#xff08;&#xff09;MAX&#xff08;&#xff09;&#xff0c;MIN&#xff08;&#xff09;GROUP_CONCAT() 2.1.…

前端学习杂乱记录

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、Html二、CSS1. BFC布局2. 定位总结3. 动画1. transform变换2. transition过渡3. keyframes 和 animation 3. 伸缩盒模型&#xff1a;flex布局 三、JS1. 逻辑中断…

nvm安装实现node多版本的切换

nvm安装实现node多版本的切换 方式一 下载安装包安装下载安装包解压安装设置 nvm 环境变量查看 nvm 是否安装完成安装 node 环境切换 node 版本列出已经安装的版本 方式二 一键脚本安装下载安装查看 nvm 是否安装完成安装 node 环境切换 node 版本列出已经安装的版本nvm相关命令…

PyTorch中的学习率预热(warmup)

PyTorch提供了学习率调度器(learning rate schedulers)&#xff0c;用于在训练过程中实现各种调整学习率的方法。实现在torch.optim.lr_scheduler.py中&#xff0c;根据epoch数调整学习率。大多数学习率调度器可以称为背对背(back-to-back)&#xff0c;也称为链式调度器&#x…

Linux入门2

文章目录 一、Linux基本命令1.1 文件的创建和查看命令1.2 文件的复制移动删除等命令1.3 查找命令1.4 文件的筛选和管道的使用1.5 echo、tail和重定向符 二、via编辑器三、权限控制3.1 root用户&#xff08;超级管理员&#xff09;3.2 用户和用户组3.3 权限信息3.4 chmod命令 一…

Streamlit:使用 Python 快速开发 Web 应用

一、简单介绍 Streamlit 是一个开源 Python 库&#xff0c;官网地址&#xff1a; https://streamlit.io/http://StreamlitStreamlit 是一个开源的 Python 框架&#xff0c;旨在为数据科学家和 后端工程师们提供只需几行代码即可创建动态数据应用的功能。 让没有任何前端基础…

C#软键盘设计字母数字按键处理相关事件函数

应用场景&#xff1a;便携式设备和检测设备等小型设备经常使用触摸屏来代替键盘鼠标的使用&#xff0c;因此在查询和输入界面的文本或者数字输入控件中使用软件盘来代替真正键盘的输入。 软键盘界面&#xff1a;软键盘界面实质上就是一个普通的窗体上面摆放了很多图片按钮&…

使用SpringCloud构建可伸缩的微服务架构

Spring Cloud是一个用于构建分布式系统的开源框架。它基于Spring Boot构建&#xff0c;并提供了一系列的工具和组件&#xff0c;用于简化开发分布式系统的难度。Spring Cloud可以帮助开发人员快速构建可伸缩的微服务架构。 要使用Spring Cloud构建可伸缩的微服务架构&#xff0…

实时监控局域网计算机桌面怎么设置!五个可实现的方法分享,绝了!

员工在工作时间里究竟在做什么&#xff1f;他们的网络活动是否合规&#xff1f;如何确保敏感信息不被泄露&#xff1f; 在企业管理层面&#xff0c;实时监控局域网计算机桌面已成为提升工作效率、确保数据安全与规范员工行为的重要手段。 技术的不断进步&#xff0c;多种解决…

【C++进阶】map和set的使用

【C进阶】map和set的使用 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C&#x1f96d; &#x1f33c;文章目录&#x1f33c; 1. 序列式容器和关联式容器 2. set系列的使用 2.1 set 和 multiset 2.2 set 类的介绍 2.3 set 的构造和…

【Linux篇】常用命令及操作技巧(基础篇)

&#x1f30f;个人博客主页&#xff1a;意疏-CSDN博客 希望文章能够给到初学的你一些启发&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏支持一下笔者吧&#xff5e; 阅读指南&#xff1a; 开篇说明帮助命令常见的七个linux操作终端实用的技巧跟文件目录…

C++11之统一的列表初始化

一.{}初始化 在c98中&#xff0c;标准允许使用{}对数组或结构体元素进行统一的列表初始值设定&#xff1a; struct mess {int _x;string _str; }; int main() {//注意&#xff0c;使用new的一定是指针int* arr new int[4] {1, 2, 3, 4};//数组初始化int arr[] { 1,3,5,6 };…

基于Spring Boot和Vue的私人牙科诊所系统的设计与实现(毕业论文)

目 录 1 前言 1 1.1 研究目的与意义 1 1.2 国内外研究概况 1 1.3 主要研究内容 2 1.4 论文结构 3 2 系统分析 3 2.1 可行性分析 3 2.1.1 技术可行性分析 3 2.1.2 经济可行性分析 3 2.1.3 操作可行性分析 4 2.1.4 法律可行性分析 4 2.2 需求分析 4 2.2.1 管理员需求分析 4 2.2.2…

3.1 数据表的基本查询

我们学习的怎么管理逻辑空间&#xff0c;怎么创建数据表&#xff0c;怎么定义字段&#xff0c;怎么创建索引&#xff0c;这些都是DDL语句。从本次课开始&#xff0c;我们来学习DML语句&#xff0c;也就是该如何增删改查操作数据。我们学习DML语句的前提是数据表要有足够多的数据…

Moving Elevator System Fully functional

这是一个功能齐全的电梯系统,配有电梯箱车、电梯井、电缆和每层的门框 电梯完全被操纵,有动画门、电缆线、滑轮系统。 还有几个C#脚本文件控制电梯、门和灯。 此套餐还包括相关声音,如电梯移动、门打开/关闭、楼层铃叮。 电梯车厢有工作门和按钮,车顶还有一个逃生舱口。 每…

低投入、高效率 基于PHP+MySQL组合开发的求职招聘小程序源码系统 带完整的安装代码包以及搭建部署教程

系统概述 这款求职招聘小程序源码系统是专门为求职招聘领域打造的综合性平台。它利用 PHP 强大的编程语言特性和 MySQL 稳定的数据存储功能&#xff0c;实现了一个功能齐全、性能优越的求职招聘系统。 整个系统架构设计合理&#xff0c;具备良好的扩展性和兼容性。无论是小型…

从《中国数据库前世今生》看中国数据库技术的发展与挑战

从《中国数据库前世今生》看中国数据库技术的发展与挑战 引言 在当今数字化浪潮中&#xff0c;数据库技术已成为支撑全球经济运行的核心基础设施。作为程序员&#xff0c;我一直对数据库技术的发展充满好奇。《中国数据库前世今生》纪录片深入探索了中国数据库技术的演变历程…

【Python报错已解决】libpng warning: iccp: known incorrect sRGB profile

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

怎么把图片压缩变小?把图片压缩变小的八种压缩方法介绍

怎么把图片压缩变小&#xff1f;在当今这个信息高度共享的时代&#xff0c;图片不仅仅是简单的视觉元素&#xff0c;它们承载着我们的记忆、故事和创意。无论是旅行的风景、家庭的聚会&#xff0c;还是工作中的项目展示&#xff0c;图片都在我们的生活中扮演着不可或缺的角色。…