初识Linux · 地址空间

news2024/11/19 1:36:40

目录

前言:

代码现象

快速理解该现象

理解部分细节问题

细节1 拷贝和独立性

细节2 如何理解地址空间

细节3 为什么存在地址空间

细节4 如何进一步理解页表和写时拷贝


前言:

本文介绍的是有关地址空间,咱们的介绍的大体思路是,先直接看现象,通过现象我们引入地址空间的概念,然后是简单的介绍地址空间,如何理解地址空间等,在往后的学习中,我们大概会介绍地址空间3 - 4次,因为地址空间有很多内容一次性还介绍不完,并且在本文中,我们能够理解之前颠覆代码三观的函数——fork(),现在就进入正题。


代码现象

目前我们对于地址空间没有明确的概念,所以先来看这样一段代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<sys/types.h>

int g_val =100;
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                int count = 1;
                while(count)
                {

                printf("g_val is %d,&g_val = %p\n",g_val,&g_val);
                sleep(1);
                if(count == 5)
                        g_val = 200;
                count++;
                }

        }
        else
        {
                while(1)
                {
                printf("g_val is %d,&g_val = %p\n",g_val,&g_val);
                sleep(1);
                }
        }

        return 0;
}

代码的意思是,我们创建一个父进程之后,在父进程里面创建一个子进程,子进程要完成的工作是打印g_val和它的地址,当count到5的时候就修改g_val,但是后续还是要一直打印,父进程要做的工作就是一直打印g_val和g_val的地址。

现象如下:

打印5秒之后,g_val的值如愿以偿的被修改了,此时让父进程打印的时候,我们发现一个怪事,打印的时候为什么父进程中的g_val没有变化呢?我们在进程部分知道父进程的数据是和子进程共享的,但是此时,父进程的数据被子进程修改了,父进程居然无动于衷?

现在的现象就是:一个变量,地址没有变化,但是拥有两个不同的值。

我们一会儿要理解的就是该现象,该现象理解了之后,我们同时就能理解fork函数的返回值是怎么回事了。


快速理解该现象

我们如果想要快速理解该现象,就需要引入两个概念,一个是物理内存,一个是虚拟内存

物理内存很好理解,即我们平常买的,比如三星SSD 990Pro内存条,就是物理内存,数据是实打实的在里面加载了的,但是虚拟内存是怎么回事呢?

我们平常写下的对某个对象取地址,本质上都是在虚拟内存层面上进行操作,并不是在物理内存上,那么上面代码写的,&g_val本质上就是个虚拟地址!

那么虚拟地址(内存)如何和物理地址(内存)进行联系呢?

这里就需要引出地址空间的概念了,这是地址空间的形象图,我们在语言学习阶段,最多涉及到的只有malloc空间在堆上,局部变量在栈上等概念,我们没有系统的学习,这里我们会深入一点点,为什么存在地址空间?地址空间是什么?有什么用?这是我们需要知道的。

我们最开始的问题是,虚拟内存如何和物理内存进行联系的,这个过程有地址空间的一份功劳,我们从名字来看,地址空间,地址,空间,容易想到这是一块空间,空间里面充满了地址这种描述,实际上确实是这样的,地址空间在源码中的名字叫做mm_struct,更深层次的我们不追究,而我们最开始说的,task_struct,mm_struct是嵌套在里面的。

通过我们刚才的描述,mm_struct里面充满了地址,那么是谁的地址呢?

由图我们知道,由堆区的地址,栈的地址,初始化数据的地址等,但是同时,不是所有的地址我们都是可以访问的,像内核空间的地址,我们知道,但是是无法访问的。一个空间里面充满了地址,可以用什么变量表示呢?结构体对吧!

所以地址空间本质上就是结构体,进程通过地址空间的所有地址来找到物理内存中对应的数据。

那么问题又来了,里面不是都存的虚拟地址吗,怎么通过虚拟地址来找物理地址呢?

这时候,页表就该引入了。

页表,最直观的就是:

最最最简单的页表就是这样,每一行的元素存在映射关系,既然是提到了映射关系,相信不少同学都明白了,映射嘛,左边是虚拟地址,右边是物理地址,这就是OS通过页表,地址空间等,链接了虚拟内存和物理内存的方法。

当然了,页表还有许多没有介绍的,在后续文章中会介绍的。

现在就得出结论:虚拟内存和物理内存的联系是通过页表,地址空间,从地址空间得到虚拟内存,在页表存在虚拟内存和物理内存的映射的关系来找到对应的数据,这是OS中找数据的方法。

现在只是知道了找数据,但是我们不清楚了找数据之后,为什么同样的地址会有两份结果,那么虚拟地址都是一样的,映射关系可以不一样吗?

大体图就是这样,父进程和子进程得到的虚拟地址是一样的,但是呢,当子进程对父进程中的某个数据发生了修改,此时就会发生写时拷贝,即在物理内存层面拷贝一个该数据,子进程指向该数据即可。

以上是对文章开头代码的简单理解。


理解部分细节问题

细节1 拷贝和独立性

我们重新理解一下,什么是进程的独立性?

从上文代码可以看出来,我们虽然在父进程里面创建了一个子进程,但是数据在物理内存中却不是同一个,通过写时拷贝再次印证了进程的独立性问题。

既然子进程修改父进程中的数据,会导致发生写时拷贝,那么在物理内存层面,为什么不全部拷贝一份呢?

在C++的类和对象中,存在按需实例化的语法,在OS层面上也是同理,从按需实例化的角度来看,子进程需要使用到父进程中的什么数据,如果发生了改变才会有写时拷贝,从地址空间的角度来看,地址空间的内核部分,是用户层面无法调用的,所以没有必要发生拷贝复制,所以按需实例化的方式,成功的可以节省一定的空间。

细节2 如何理解地址空间

在小学的时候,我们和同桌不妨都有过三八线的经历,这个动作的本质就是在划分区域,划分区域涉及到的对象有自己拥有的区域,桌子这一整个对象,所以这个动作可以分为,A拥有自己的空间,B拥有自己的空间,所有空间加起来就是桌面的大小。

那么我们不妨将桌面的整个空间理解为OS内核,里面存在的所有地址空间,都是一个一个的结构体,那么为了区域划分,结构体里面肯定是需要不同的变量来表示区域的开始 结束的,在地址空间这里,我们不妨简单看一下源码:

struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs,指向线性区对象的链表头部 */
	struct rb_root mm_rb;                   /* 指向线性区对象的红黑树*/
	struct vm_area_struct * mmap_cache;	/* last find_vma result 指向最近找到的虚拟区间 */
#ifdef CONFIG_MMU 
 
/*用来在进程地址空间中搜索有效的进程地址空间的函数*/
 
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
/*释放线性区的调用方法*/
 void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
	unsigned long mmap_base;		/* base of mmap area ,内存映射区的基地址*/
	unsigned long task_size;		/* size of task vm space */
	unsigned long cached_hole_size; 	/* if non-zero, the largest hole below free_area_cache */
	unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */
	pgd_t * pgd;                            /* 页表目录指针*/
	atomic_t mm_users;			/* How many users with user space?,共享进程的个数 */
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */
	int map_count;				/* number of VMAs ,线性区个数*/
	struct rw_semaphore mmap_sem;
	spinlock_t page_table_lock;		/* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/
 
	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */
 
 
	unsigned long hiwater_rss;	/* High-watermark of RSS usage,进程拥有的最大页表数目 */
	unsigned long hiwater_vm;	/* High-water virtual memory usage ,进程线性区的最大页表数目*/
 
	unsigned long total_vm, locked_vm, shared_vm, exec_vm;
	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
	unsigned long start_code, end_code, start_data, end_data;     /*维护代码区和数据区的字段*/
	unsigned long start_brk, brk, start_stack;       /*维护堆区和栈区的字段*/
	unsigned long arg_start, arg_end, env_start, env_end;  /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/
 
	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
 
	/*
	 * Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	struct mm_rss_stat rss_stat;
 
	struct linux_binfmt *binfmt;
 
	cpumask_t cpu_vm_mask;
 
	/* Architecture-specific MM context */
	mm_context_t context;
 
	/* Swap token stuff */
	/*
	 * Last value of global fault stamp as seen by this process.
	 * In other words, this value gives an indication of how long
	 * it has been since this task got the token.
	 * Look at mm/thrash.c
	 */
	unsigned int faultstamp;
	unsigned int token_priority;
	unsigned int last_interval;
 
	unsigned long flags; /* Must use atomic bitops to access the bits */
 
	struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
	spinlock_t		ioctx_lock;
	struct hlist_head	ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
	/*
	 * "owner" points to a task that is regarded as the canonical
	 * user/owner of this mm. All of the following must be true in
	 * order for it to be changed:
	 *
	 * current == mm->owner
	 * current->mm != mm
	 * new_owner->mm == mm
	 * new_owner->alloc_lock is held
	 */
	struct task_struct *owner;
#endif
 
#ifdef CONFIG_PROC_FS
	/* store ref to file /proc/<pid>/exe symlink points to */
	struct file *exe_file;
	unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
	struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};

里面存在的字段,如start_code等,都是地址空间里面不同区域的划分。

这里我们可以得出结论,地址空间本质就是内核中的一个一个的结构体,每个进程都拥有自己的地址空间

细节3 为什么存在地址空间

结合细节2,我们以大富翁的概念来结束进程的理解,大富翁的总资产是10亿,对于不同的孩子,他都是说我有10亿,你们看着要就行,但是大富翁肯定是会给儿女不同的零花钱什么的。这里面,大富翁就是OS,地址空间就是零花钱,不同的儿女对应不同的进程。

那么为什么存在地址空间呢?它存在的意义是什么?

从内存层面上来讲,如何程序直接在物理内存上开辟空间,那必然是杂乱无章,因为哪里有空间就开在哪里,操作系统管理起来就十分麻烦,这是无序的,有了地址空间这个结构,地址空间里面存储的都是进程里面的地址信息,那么集合管理在一个结构体里面,从有序的角度去看待地址,而非在物理内存里面无序的查找。所以地址空间可以让无需变成有序。

从管理内存和进程的角度来看,地址空间的存在可以让进程管理模块和进程管理模块解耦,如果没有地址空间,那么进程是直接链接在物理内存上的,那么进程里面申请了一个变量,在物理内存上就一定会申请空间,势必空间会不太够用,但是有地址空间,即便申请了,但是没有用,页表那里甚至可以先不映射,如果使用了,再映射即可。所以管理进程和内存,可以通过页表来解耦,而不是直接让进程和内存完全绑定在一起。

从安全问题上来看,我们之前写代码的时候,如果出现了非法请求,比如野指针访问,进程就会被直接杀死,这是因为地址空间已经划分好了空间,如果访问的地址超过了这个空间,就是非法访问,OS层面检测出越界,肯定就直接杀死该进程了。这是一种对物理内存的保护,再比如,一对父子,子如果直接拥有钱,自由支配,不免的会买不利于成长的玩具,但是父如果作为中间商,对钱进行管理,子想要买,必须通过父这一层,此时,子想买扑克牌,父制止,这个就是对非法请求的拦截,也是保护了整个内存的运作。

细节4 如何进一步理解页表和写时拷贝

我们看一段简单的代码:

const char* str = "abcdefg";
str = 'A';

为什么str不能被修改呢?我们知道它是const类型的。

但是为什么const类型就不能被修改呢?因为在页表里面还存在数据的权限,rwx。

对于str来说,是只读的,所以对应的权限是r,代码一般也是只读的。所以即便页表对应到了该数据,但是页表中记录的权限仍然是r,没有w,那么它就无法写。

对于页表中是如何映射的我们先不管,里面涉及到了cpu中的CR3和MMU,后面详细介绍。

当OS在页表中查找数据时,如果没有该数据,就会发生缺页中断,如果数据需要写时拷贝,就会发生写时拷贝,对于数据,如果上面的两种情况都不满足,才会引入异常。

这里对于虚拟地址也带一嘴,虚拟地址哪里来的呢?为什么地址空间一来就有地址呢?这是因为程序本身就有地址,我们使用指令:objdump -S 可执行文件名

0000000000001000 <_init>:
    1000:	f3 0f 1e fa          	endbr64 
    1004:	48 83 ec 08          	sub    $0x8,%rsp
    1008:	48 8b 05 d9 2f 00 00 	mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__@Base>
    100f:	48 85 c0             	test   %rax,%rax
    1012:	74 02                	je     1016 <_init+0x16>
    1014:	ff d0                	call   *%rax
    1016:	48 83 c4 08          	add    $0x8,%rsp
    101a:	c3                   	ret    

Disassembly of section .plt:

0000000000001020 <.plt>:
    1020:	ff 35 8a 2f 00 00    	push   0x2f8a(%rip)        # 3fb0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:	f2 ff 25 8b 2f 00 00 	bnd jmp *0x2f8b(%rip)        # 3fb8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102d:	0f 1f 00             	nopl   (%rax)
    1030:	f3 0f 1e fa          	endbr64 
    1034:	68 00 00 00 00       	push   $0x0
    1039:	f2 e9 e1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    103f:	90                   	nop
    1040:	f3 0f 1e fa          	endbr64 
    1044:	68 01 00 00 00       	push   $0x1
    1049:	f2 e9 d1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    104f:	90                   	nop
    1050:	f3 0f 1e fa          	endbr64 
    1054:	68 02 00 00 00       	push   $0x2
    1059:	f2 e9 c1 ff ff ff    	bnd jmp 1020 <_init+0x20>
    105f:	90                   	nop

Disassembly of section .plt.got:

0000000000001060 <__cxa_finalize@plt>:
    1060:	f3 0f 1e fa          	endbr64 
    1064:	f2 ff 25 8d 2f 00 00 	bnd jmp *0x2f8d(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    106b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

Disassembly of section .plt.sec:

0000000000001070 <printf@plt>:
    1070:	f3 0f 1e fa          	endbr64 
    1074:	f2 ff 25 45 2f 00 00 	bnd jmp *0x2f45(%rip)        # 3fc0 <printf@GLIBC_2.2.5>
    107b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

0000000000001080 <sleep@plt>:
    1080:	f3 0f 1e fa          	endbr64 
    1084:	f2 ff 25 3d 2f 00 00 	bnd jmp *0x2f3d(%rip)        # 3fc8 <sleep@GLIBC_2.2.5>
    108b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

0000000000001090 <fork@plt>:
    1090:	f3 0f 1e fa          	endbr64 
    1094:	f2 ff 25 35 2f 00 00 	bnd jmp *0x2f35(%rip)        # 3fd0 <fork@GLIBC_2.2.5>
    109b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

就可以看到对应的程序地址了,当然,了解即可。

地址空间到这里就粗略的收场了,后面会介绍的。

再回归到最开始的问题,为什么fork的返回值会有“两个”,你理解了吗?


感谢阅读!

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

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

相关文章

pyqt打包成exe相关流程

1、首先是安装pyinstaller, 在cmd中输入以下安装命令&#xff1a; pip3 install pyinstaller -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/ 2、安装完毕之后&#xff0c;下一步就是找到你要打包的工程&#xff0c;打包的logo放置如下位置&#xff1a; 3、将log…

甘肃小米醋:醇厚滋味,传承之香

宝子们&#xff0c;今天来给大家介绍一下甘肃的小米醋。✨甘肃小米醋&#xff0c;那是一种带着浓郁地域特色的美味宝藏。&#x1f381;&#x1f33e;以优质小米为原料&#xff0c;经过传统工艺的精心酿造&#xff0c;成就了这一瓶瓶醇厚的小米醋。打开瓶盖&#xff0c;那独特的…

class 028 基数排序

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。 这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐. https://space.bilibili.com/8888480?spm_id_f…

Redis string类型hash类型

string类型 类型介绍 在Redis中的所有的key都是string类型&#xff0c;而value的类型有多种。 Redis中的字符串是直接按照二进制的方式进行存储的&#xff0c;也就是不会做任何的编码转换&#xff0c;存的是什么&#xff0c;取出来的就是什么。这样一般来说&#xff0c;Redi…

《程序猿之Redis缓存实战 · 位图类型》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

设计效率拉高!一键生成国庆主题3d立体字!

​ 一键生成国庆主题字&#xff01;这是数字75&#xff0c;上传&#xff0c;点击生成 等个几秒&#xff0c;75周年立体字就做好了 而且融入了各种中国古建筑元素 这样的国庆主题字效果&#xff0c;以前我们要用c4d建模然后渲染出效果图 费时费力才做出一张 现在我们工作流…

事实与价值双阈值是算计启动的门槛

在现代社会&#xff0c;个体与群体的决策过程受到多种因素的影响&#xff0c;其中事实与价值的关系尤为重要。事实作为客观存在的基础&#xff0c;价值则是主观认知的体现。两者的相互作用构成了人类行为的复杂性&#xff0c;尤其在经济学、社会学以及伦理学等领域&#xff0c;…

JAVA毕业设计182—基于Java+Springboot+vue3的河道治理管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvue3的河道治理管理系统(源代码数据库)182 一、系统介绍 本项目前后端分离(可以改为ssm版本)&#xff0c;分为用户、工作人员、管理员三种角色 1、用户&#x…

linux 系统磁盘空间查看与清理

正常清理步骤 首先查看文件和目录的使用空间&#xff0c;系统/根目录下的文件夹一般情况不会占用大的磁盘空间&#xff0c;因此可主要查看您创建的目录或文件等 文件大小 使用ls -alh命令来查看&#xff0c;比如下方的.bashrc、.profile文件的大小。但是看到的文件夹大小仅仅…

Spire.PDF for .NET【页面设置】演示:设置 PDF 的查看器首选项和缩放系数

优化查看器首选项和缩放因子对于改善 PDF 文档的查看体验至关重要。通过使用适当的查看器首选项和缩放因子&#xff0c;您可以使您的 PDF 文档更加用户友好、可查看且适合不同的设备和平台。在本文中&#xff0c;我们将演示如何使用Spire.PDF for .NET在 C# 和 VB.NET 中为 PDF…

【计算机网络】详解HTTP请求和响应格式常见请求方法Header报头响应报文状态码URL

一、HTTP协议的定义 在互联网世界中&#xff0c;HTTP &#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;是一个至关重要的协议。它定义了客户端&#xff08;如浏览器&#xff09;与服务器之间如何通信&#xff0c;以交换或传输超文本&#xff08…

毕业设计选题:基于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包&#…

召回11 地理位置召回、作者召回、缓存召回

有用但重要性不高 地理位置召回 GeoHash召回&#xff1a;对身边周围的事情感兴趣 GeoHash把经纬度编码成二进制哈希码方便检索。召回只根据经纬度这个地理位置&#xff0c;返回一批优质笔记&#xff0c;完全不考虑用户兴趣&#xff0c;也是因此返回优质笔记&#xff0c;大概…

[Docker学习笔记]Docker的原理Docker常见命令

文章目录 什么是DockerDocker的优势Docker的原理Docker 的安装Docker 的 namespaces Docker的常见命令docker version:查看版本信息docker info 查看docker详细信息我们关注的信息 docker search:镜像搜索docker pull:镜像拉取到本地docker push:推送本地镜像到镜像仓库docker …

安卓13设置删除网络和互联网选项 android13隐藏设置删除网络和互联网选项

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改4.1修改方法14.2修改方法25.编译6.彩蛋1.前言 有些客户不想让用户修改默认的网络配置,禁止用户进入里面调整网络相关的配置。 2.问题分析 像这个问题,我们有好几种方法去处理,这种需求一般…

PyGWalker:让你的Pandas数据可视化更简单,快速创建数据可视化网站

1、PyGWalker应用: 在数据分析的过程中,数据的探索和可视化是至关重要的环节,如何高效地将分析结果展示给团队、客户,甚至是公众,是很多数据分析师和开发者面临的挑战,接下来介绍的两大工具组合——PyGWalker与Streamlit,可以帮助用户轻松解决这个问题,即使没有复杂的代…

java 洛谷题单【数据结构1-1】线性表

P3156 【深基15.例1】询问学号 解题思路 很简单的一道题&#xff0c;但是由于n、m的数据很大&#xff0c;要用Java的I/O流读入和输出。 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; impo…

【springboot】使用thymeleaf模板

1. 导入依赖 首先&#xff0c;创建一个Spring Boot项目&#xff0c;并添加Thymeleaf依赖。在pom.xml文件中添加以下依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifa…

9.28 daimayuan 模拟赛总结

感觉 -S 模拟赛时间好紧啊 复盘 8:00 开题 扫了一遍四道题&#xff0c;感觉 T1 很典&#xff0c;T2 有点神秘&#xff0c;T3 计数&#xff0c;但限制是简单的&#xff0c;看上去非常可做&#xff1b;T4 也有点神秘 推 T1&#xff0c;先定根&#xff0c;然后树形dp是显然的&…

【Android】Jetpack组件之LifeCycle

引言 Lifecycle组件是Android Jetpack架构组件之一&#xff0c;它提供了一种方法来管理Android组件&#xff08;如Activity、Fragment和服务&#xff09;的生命周期。Lifecycle组件帮助你执行与生命周期相关联的操作&#xff0c;确保在适当的时间发生适当的事情&#xff0c;例…