深入理解Linux网络随笔(一):内核是如何接收网络包的(下篇)

news2025/2/15 7:19:47

3、接收网络数据

3.1.1硬中断处理

数据帧从网线到达网卡时候,首先到达网卡的接收队列,网卡会在初始化时分配给自己的RingBuffer中寻找可用内存位置,寻找成功后将数据帧DMA到网卡关联的内存里,DMA操作完成后,网卡会向CPU发起一个硬中断,通知CPU有数据到达。

在这里插入图片描述

启动网卡到硬中断注册处理函数调用流程ign_open-->igb_request_irq-->igb_request_msix-->igb_msix_ring

static irqreturn_t igb_msix_ring(int irq, void *data)
{
	struct igb_q_vector *q_vector = data;

	/* Write the ITR value calculated from the previous interrupt. */
    //记录硬件中断频率
	igb_write_itr(q_vector);
    //调度NAPI机制
	napi_schedule(&q_vector->napi);

	return IRQ_HANDLED;
}

napi_schedule将q_vector关联的NAPI结构添加于调度队列,函数调用关系napi_schedule-->__napi_schedule-->____napi_schedule

static inline void ____napi_schedule(struct softnet_data *sd,
				     struct napi_struct *napi)
{
	......
	list_add_tail(&napi->poll_list, &sd->poll_list);
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

基于软中断的NAPI处理调用list_add_tail修改Per-CPU变量的softnet_datapoll_list,将驱动napi_struct传入的poll_list添加于软中断的poll_list,触发NET_RX_SOFTIRQ类型软中断。

void __raise_softirq_irqoff(unsigned int nr)
{
	//禁中断
    lockdep_assert_irqs_disabled();
    //追踪软中断
	trace_softirq_raise(nr);
    //触发
	or_softirq_pending(1UL << nr);
}
#define or_softirq_pending(x)  (S390_lowcore.softirq_pending |= (x))

通过or操作符将软中断nr对应的位设置为1,调用or_softirq_pending将该标志位添加于软中断挂起队列,触发软中断。

3.1.2软中断处理

前文分析软中断处理通过ksfortirq内核线程处理,会调用两个函数ksoftirqd_should_runrun_ksoftirqd,均调用local_softirq_pending进行处理。

在这里插入图片描述

#define local_softirq_pending() (S390_lowcore.softirq_pending)
static int ksoftirqd_should_run(unsigned int cpu)
{
	return local_softirq_pending();
}
static void run_ksoftirqd(unsigned int cpu)
{
	ksoftirqd_run_begin();
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();
		ksoftirqd_run_end();
		cond_resched();
		return;
	}
	ksoftirqd_run_end();
}

硬中断处理是由硬件中断服务例程(ISR)触发的,调用local_softirq_pending标记软中断挂起状态,真正的中断处理由ksfortirq内核线程处理,函数调用逻辑run_ksoftirqd-->__do_softirq

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    .....
    // 获取当前 CPU 上待处理的软中断类型的掩码
    pending = local_softirq_pending();
    // 遍历所有待处理的软中断
    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;
        // 将指针 'h' 移动到当前待处理软中断类型的处理函数
        h += softirq_bit - 1;
        // 计算当前软中断处理函数在软中断处理数组中的索引
        vec_nr = h - softirq_vec;  
        // 获取当前任务的预占用计数
        prev_count = preempt_count();
        // 统计当前软中断类型的处理次数
        kstat_incr_softirqs_this_cpu(vec_nr);
        // 调用 trace 函数跟踪软中断的进入
        trace_softirq_entry(vec_nr);
        // 执行软中断的处理函数
        h->action(h);
        // 调用 trace 函数跟踪软中断的退出
        trace_softirq_exit(vec_nr);
        ......
        // 处理下一个软中断类型
        h++;
        // 右移 pending 位图,检查下一个待处理的软中断
        pending >>= softirq_bit;
    }
}

__do_softirq根据传入的软中断类型处理所有挂起的软中断,通过h->action(h)执行具体的软中断处理函数。硬中断中的设置软中断标记,和ksoftirqd中的判断是否有软中断到达,都是基于smp_processor_id()的。只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的,针对Linux软中断消耗集中一个核现象,方法:调整硬中断CPU亲和性,硬中断打散于不同核上。

设备初始化时调用open_softirq(NET_RX_SOFTIRQ, net_rx_action),将网络接收软中断 NET_RX_SOFTIRQ 绑定到 net_rx_action 处理函数,收到软中断类型NET_RX_SOFTIRQ会调用net_rx_action接收软中断,处理网络数据包。

static __latent_entropy void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);  // 获取当前 CPU 的软中断数据
    unsigned long time_limit = jiffies +
        usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));  // 计算软中断的超时时间
    int budget = READ_ONCE(netdev_budget);  // 获取软中断的处理预算(每次允许处理的最大数据包数量)
    LIST_HEAD(list);  // 创建链表 list,用于存储要处理的 napi 结构体
    LIST_HEAD(repoll);  // 创建链表 repoll,用于存储需要重新投递的 napi 结构体

    local_irq_disable();  // 关闭CPU硬中断
    list_splice_init(&sd->poll_list, &list);  // 将当前 CPU 上的 poll_list 中的元素移动到 list 中
    local_irq_enable();  // 重新启用本地中断

    for (;;) {
        struct napi_struct *n;

        skb_defer_free_flush(sd);  // 清理延迟释放的数据包

        if (list_empty(&list)) {  // 如果没有要处理的 napi 结构体
            if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))  // 如果没有需要处理的 RPS IPI 和 repoll 列表为空
                goto end;  // 结束处理
            break;  // 如果有需要的工作,继续处理
        }

        n = list_first_entry(&list, struct napi_struct, poll_list);  // 获取待处理的第一个 napi 结构体
        budget -= napi_poll(n, &repoll);  // 调用 napi_poll 处理数据包,更新剩余预算

        /* 如果软中断窗口已耗尽,则退出处理
         * 允许最多运行 2 个 jiffies,这会允许平均延迟为 1.5/HZ
         */
        if (unlikely(budget <= 0 ||
                     time_after_eq(jiffies, time_limit))) {
            sd->time_squeeze++;  // 记录时间压缩(即软中断处理超时)
            break;  // 退出循环
        }
    }

    local_irq_disable();  // 禁用本地中断

    // 将 repoll 和 list 的元素合并到 sd->poll_list 中
    list_splice_tail_init(&sd->poll_list, &list);
    list_splice_tail(&repoll, &list);
    list_splice(&list, &sd->poll_list);

    // 如果 poll_list 中仍有元素,重新唤起软中断
    if (!list_empty(&sd->poll_list))
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);

    net_rps_action_and_irq_enable(sd);  // 处理 RPS(Receive Packet Steering)和启用中断
end:;
}

遍历所有待处理的网络接收软中断,核心获取当前CPU软中断数据softnet_datalist_first_entry遍历poll_list,调用 napi_poll 处理数据包,上文中分析了NAPI机制的poll函数是igb_poll

static int igb_poll(struct napi_struct *napi, int budget)
{
.....
    //TX发送队列
	if (q_vector->tx.ring)
		clean_complete = igb_clean_tx_irq(q_vector, budget);
    //RX接收队列
	if (q_vector->rx.ring) {
		int cleaned = igb_clean_rx_irq(q_vector, budget);
     // 累计本次轮询处理的数据包数量
		work_done += cleaned;
		if (cleaned >= budget)
			clean_complete = false;
	}
......
	return work_done;
}

igb_polligb 网卡驱动中 NAPI 轮询的核心函数,负责清理发送和接收队列的中断。核心处理逻辑igb_clean_rx_irqigb_clean_tx_irq

static int igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
	......
    rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean);  // 获取当前的接收描述符
    size = le16_to_cpu(rx_desc->wb.upper.length);  // 获取数据包的大小
    rx_buffer = igb_get_rx_buffer(rx_ring, size, &rx_buf_pgcnt);  // 获取接收缓冲区
    pktbuf = page_address(rx_buffer->page) + rx_buffer->page_offset;  // 获取数据包的缓冲区地址
    if (!skb) {
    unsigned char *hard_start = pktbuf - igb_rx_offset(rx_ring);  // 获取数据包的起始地址
    unsigned int offset = pkt_offset + igb_rx_offset(rx_ring);  // 计算数据包的偏移

    xdp_prepare_buff(&xdp, hard_start, offset, size, true);  // 为 XDP 准备数据包
    xdp_buff_clear_frags_flag(&xdp);  // 清除 XDP 的分段标志

    skb = igb_run_xdp(adapter, rx_ring, &xdp);  // 运行 XDP 处理,构建 skb
}
    if (IS_ERR(skb)) {
    unsigned int xdp_res = -PTR_ERR(skb);

    if (xdp_res & (IGB_XDP_TX | IGB_XDP_REDIR)) {  // 如果是 XDP 传输或重定向
        xdp_xmit |= xdp_res;
        igb_rx_buffer_flip(rx_ring, rx_buffer, size);  // 切换缓冲区
    } else {
        rx_buffer->pagecnt_bias++;  // 增加页面计数偏移
    }
    total_packets++;  // 增加数据包计数
    total_bytes += size;  // 增加字节数
}else if (skb) {
    igb_add_rx_frag(rx_ring, rx_buffer, skb, size);  // 将接收到的数据包添加到 skb 中
}
    napi_gro_receive(&q_vector->napi, skb);  // 将 skb 传递给 NAPI 进行进一步处理
    igb_put_rx_buffer(rx_ring, rx_buffer, rx_buf_pgcnt);  // 释放接收缓冲区
    if (cleaned_count)
		igb_alloc_rx_buffers(rx_ring, cleaned_count);
    ......
}

igb_clean_tx_irq核心将数据帧从RingBuffer中摘下,igb_alloc_rx_buffers重新申请新的skb再重新挂起,NAPI机制下一步处理调用napi_gro_receive

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	......
	ret = napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
	......
	return ret;
}
EXPORT_SYMBOL(napi_gro_receive);

napi_gro_receive用于网卡GRO特性,合并多个小的数据包(通常是同一流的 TCP 数据包)为一个较大的数据包,从而减少协议栈的处理开销。调用napi_skb_finish完成GRO处理。

static gro_result_t napi_skb_finish(struct napi_struct *napi,
				    struct sk_buff *skb,
				    gro_result_t ret)
{
	switch (ret) {
	case GRO_NORMAL:
		gro_normal_one(napi, skb, 1);
		break;

	case GRO_MERGED_FREE:
		if (NAPI_GRO_CB(skb)->free == NAPI_GRO_FREE_STOLEN_HEAD)
			napi_skb_free_stolen_head(skb);
		else if (skb->fclone != SKB_FCLONE_UNAVAILABLE)
			__kfree_skb(skb);
		else
			__kfree_skb_defer(skb);
		break;

	case GRO_HELD:
	case GRO_MERGED:
	case GRO_CONSUMED:
		break;
	}

	return ret;
}

正常数据包处理GRO_NORMAL,需要合并的数据包处理GRO_MERGED_FREE,根据不同的方式选择不同的释放方式,函数调用逻辑gro_normal_one-->gro_normal_list-->netif_receive_skb_list_internal-->__netif_receive_skb_list-->__netif_receive_skb_core,数据包发送于协议栈。

3.1.3网络协议栈处理
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
				    struct packet_type **ppt_prev)
{
    ......
    // 遍历全局的协议类型链表并传递 skb,tcpdump入口
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }
    type = skb->protocol;
    
	// 如果没有精确匹配,处理协议类型
	if (likely(!deliver_exact)) {
    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                           &ptype_base[ntohs(type) & PTYPE_HASH_MASK]);
}

	// 传递到设备特定协议链表
	deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                       &orig_dev->ptype_specific);

	// 如果 skb 的设备不是原始设备,进行协议处理
	if (unlikely(skb->dev != orig_dev)) {
    deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
                           &skb->dev->ptype_specific);
	}  
}

ptype_all 是一个全局链表,包含了所有已注册的协议类型及其处理回调,在 __netif_receive_skb_core 函数中,首先会遍历这个链表,依次处理每个协议类型,并调用与协议相关的处理函数,ptype_base 是一个基于协议类型的哈希表,它包含了协议类型(如 IPv4、IPv6、TCP 等)对应的特定处理函数,当数据包的协议类型与哈希表中的某个条目匹配时,数据包会被传递到该处理函数。例如,ip_rcv 的地址通常是保存在 ptype_base 哈希表中的。

static inline int deliver_skb(struct sk_buff *skb,
			      struct packet_type *pt_prev,
			      struct net_device *orig_dev)
{
	......
    //调用协议处理函数 (pt_prev->func) 并将 skb 传递给它
	return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
};

deliver_skb 将接收到的 skb传递给协议处理函数。从 packet_type 结构体中获取 func 字段,并将 skb 数据包传递给该函数进行处理。这里的 pt_prev->func 是一个协议回调函数,指向处理该协议类型数据包的函数(例如,对于 IPv4 数据包,func 指向 ip_rcv 函数)。

3.1.4IP层处理

数据包经过协议栈处理后会被传递到IP层进行处理,收包方向IP层入口函数ip_rcv

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
	   struct net_device *orig_dev)
{
	struct net *net = dev_net(dev);

	skb = ip_rcv_core(skb, net);
	if (skb == NULL)
		return NET_RX_DROP;

	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
}

接收到的数据包会经过ip_rcv_core进行基本处理,例如对数据包进行有效性检查、协议解析等,处理成功会触发IPV4数据包Netfilter钩子链,在 NF_INET_PRE_ROUTING 钩子处插入数据包处理,NF_INET_PRE_ROUTING 是所有接收数据包到达的第一个 hook 触发点,在路由判断之前执行,对应的回调函数ip_rcv_finish

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct net_device *dev = skb->dev;
	int ret;

	/* if ingress device is enslaved to an L3 master device pass the
	 * skb to its handler for processing
	 */
	skb = l3mdev_ip_rcv(skb);
	if (!skb)
		return NET_RX_SUCCESS;

	ret = ip_rcv_finish_core(net, sk, skb, dev, NULL);
	if (ret != NET_RX_DROP)
		ret = dst_input(skb);
	return ret;
}

ip_rcv_finish负责接收和处理通过 IPv4 协议栈传输的网络数据包,核心的IP数据包处理函数调用dst_input传递数据包。

static inline int dst_input(struct sk_buff *skb)
{
	return INDIRECT_CALL_INET(skb_dst(skb)->input,
				  ip6_input, ip_local_deliver, skb);
}

基于路由类型skb_dst(skb)选择对应的处理函数,通过INDIRECT_CALL_INET宏选择IPV4/IPV6协议数据包处理函数,IPV4选择调用ip_local_deliver

int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *	Reassemble IP fragments.
	 */
	struct net *net = dev_net(skb->dev);

	if (ip_is_fragment(ip_hdr(skb))) {
		if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
			return 0;
	}

	return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
		       net, NULL, skb, skb->dev, NULL,
		       ip_local_deliver_finish);
}
EXPORT_SYMBOL(ip_local_deliver);

ip_local_deliver处理本地IPV4数据包,接收一个网络数据包skb,调用ip_is_fragment检查是否需要进行IP分片重组,对不分片/已重组的数据包调用NF_HOOKNF_INET_LOCAL_IN处理本地接收到的数据包(并不是经过路由转发的数据包),对应的处理函数ip_local_deliver_finish

参考资料:《深入理解Linux网络》

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

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

相关文章

《只狼》运行时提示“mfc140u.dll文件缺失”是什么原因?要怎么解决?

《只狼》运行时提示“mfc140u.dll文件缺失”是什么原因&#xff1f;要怎么解决&#xff1f; 宝子们&#xff0c;是不是在玩《只狼》的时候&#xff0c;突然弹出一个提示&#xff1a;“找不到mfc140u.dll文件”&#xff1f;这可真是让人着急上火&#xff01;别慌&#xff0c;今…

SSM开发(十二) mybatis的动态SQL

目录 一、为什么需要动态SQL? Mybatis 动态 sql 是做什么的? 二、多种动态 SQL 元素 三、示例 1、model定义 2、数据库定义 3、UserMapper接口及UserMapper.xml内容定义 if标签 choose/when/otherwise 标签 foreach标签 trim 标签 四、动态SQL注意 一、为什么需…

基于LVS负载均衡练习

对比 LVS 负载均衡群集的 NAT 模式和 DR 模式&#xff0c;比较其各自的优势。 NAT模式&#xff0c;全称是网络地址转换模式。NAT模式下&#xff0c;负载均衡器&#xff08;Director&#xff09;会修改请求和响应的IP地址。客户端的请求先到达Director&#xff0c;Director将请…

FreeRTOS低功耗总结

前言 Cortex-M核的MCU一般支持以下三种低功耗方式&#xff1a; ● 睡眠(Sleep)模式 ● 停止(Stop)模式 ● 待机(Standby)模式 睡眠模式 进入睡眠模式有两种指令&#xff1a;WFI(等待中断)和WFE(等待事件)&#xff0c; WFI进入睡眠模式后&#xff0c;任意中断都可唤醒。 WFE进…

【IC】AI处理器核心--第二部分 用于处理 DNN 的硬件设计

第 II 部分 用于处理 DNN 的硬件设计 第 3 章 关键指标和设计目标 在过去的几年里&#xff0c;对 DNN 的高效处理进行了大量研究。因此&#xff0c;讨论在比较和评估不同设计和拟议技术的优缺点时应考虑的关键指标非常重要&#xff0c;这些指标应纳入设计考虑中。虽然效率通常…

【python】向Jira测试计划下,附件中增加html测试报告

【python】连接Jira获取token以及jira对象 # 往 jira 测试计划下面&#xff0c;上传测试结果html def put_jira_file(plain_id):# 配置连接jiraconn ConnJira()jira conn.jira_login()[2]path jira.issue(O45- plain_id)attachments_dir os.path.abspath(..) \\test_API…

STM32自学记录(九)

STM32自学记录 文章目录 STM32自学记录前言一、DMA杂记二、实验1.学习视频2.复现代码 总结 前言 DMA 一、DMA杂记 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输&#xff0c;无须CPU干预&…

【C++】C++-教师信息管理系统(含源码+数据文件)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 【C】C教师信息管理系统&#xff08;含源码&#x…

Java Swing-5.jar 使用 jpackage 打包成 windows 可安装应用(exe,msi,免安装版exe)

环境 jdk17 (jdk14 以后自带将jar 打安装包工具 jpackage&#xff0c;版本从1.8调整到17) Maven&#xff1a;3.2.5 效果 对比 exe4j :免费版在启动的时候总是先弹出一个弹框&#xff0c;告诉用户你在用他们的免费版Launch4j:无法把jre环境打到exe文件中&#xff0c;用户需要单独…

ADC入门准备(十):信号与系统知识回顾

4.7系统函数零极点分布决定时域特性 4.7.1 H(s)极点分布与h(t)的对应图解 4.7.2 H(s)、E(s&#xff09;极点分布与自由响应、强迫响应特征的对应 4.8 H(s)零极点分布决定频域特性 4.8.1 s平面几何分析法 4.8.2 高通滤波器的频率特性 4.8.3 低通滤波器的频率特性 4.9 二阶谐振系…

wx060基于springboot+vue+uniapp的宿舍报修系统小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

CCF-GESP 等级考试 2024年9月认证C++二级真题解析

2024年9月真题 一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 正确答案&#xff1a;A 考察知识点&#xff1a;计算机存储 解析&#xff1a;磁心存储元件是早期计算机中用于存储数据的部件&#xff0c;它和现代计算机中的内存功能类似&#xff0c;都是用于临时…

第二天:工具的使用

每天上午9点左右更新一到两篇文章到专栏《Python爬虫训练营》中&#xff0c;对于爬虫有兴趣的伙伴可以订阅专栏一起学习&#xff0c;完全免费。 键盘为桨&#xff0c;代码作帆。这趟为期30天左右的Python爬虫特训即将启航&#xff0c;每日解锁新海域&#xff1a;从Requests库的…

HarmonyOS:使用List实现分组列表(包含粘性标题)

一、支持分组列表 在列表中支持数据的分组展示&#xff0c;可以使列表显示结构清晰&#xff0c;查找方便&#xff0c;从而提高使用效率。分组列表在实际应用中十分常见&#xff0c;如下图所示联系人列表。 联系人分组列表 在List组件中使用ListItemGroup对项目进行分组&#…

Django5的新特征

Django是一个用Python编写的高级Web框架&#xff0c;它的目标是让开发人员能够快速高效地构建复杂的Web应用程序。自从2008年首次发布以来&#xff0c;Django已经成为开源Web框架中的佼佼者&#xff0c;被广泛应用于各种规模的项目中。Django 提供了一套强大且全面的工具&#…

JVM类加载和垃圾回收(详细)

文章目录 JVM介绍JDK/JRE/JVM的关系 内存结构堆程序计数器虚拟机栈本地方法栈本地内存 类文件字节码文件结构 类加载类的生命周期加载类加载器双亲委派模型 链接初始化类卸载 垃圾回收堆空间的基本结构内存分配和回收原则死亡对象判断方法垃圾收集算法垃圾收集器 JVM 介绍 JD…

基于Flask的影视剧热度数据可视化分析系统的设计与实现

【FLask】基于Flask的影视剧热度数据可视化分析系统的设计与实现&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 随着互联网技术的飞速发展&#xff0c;影视剧行业的数据量呈爆炸性增长&#x…

Docker Desktop如何恢复出厂设置

在测试dify、ragfow等几个模型过程中&#xff0c;各种拉镜像建容器&#xff0c;导致错误提示“AssertionError(Can t access Redis. Please check the Redis status.)”&#xff0c;两个模型都无法使用&#xff0c;如何清空重建&#xff1f;请参照下面操作&#xff1a; 1、Win…

Android Studio:键值对存储sharedPreferences

一、了解 SharedPreferences SharedPreferences是Android的一个轻量级存储工具&#xff0c;它采用的存储结构是Key-Value的键值对方式&#xff0c;类似于Java的Properties&#xff0c;二者都是把Key-Value的键值对保存在配置文件中。不同的是&#xff0c;Properties的文件内容形…

国自然专项项目申请:AI赋能的急性心肌梗死预警研究|基金申请·25-02-14

小罗碎碎念 急性心肌梗死严重威胁生命健康&#xff0c;因其起病隐匿、发病机制复杂&#xff0c;早期预警困难。现在&#xff0c;转机来了&#xff01;国自然“AI赋能的急性心肌梗死预警研究”专项项目2025年度指南重磅发布。 该项目致力于攻克难题&#xff0c;通过多学科交叉…