当前位置: 首页 > news >正文

操作系统理解(xv6)

xv6操作系统项目复习笔记

宗旨:只记大框架,不记细节,没有那么多的时间

一、xv6的页表是如何搭建的?

xv6这个项目中,虚拟地址用了39位(27位+12位(物理内存page偏移地址)),物理地址用了56位(44位(物理内存page首地址)+12位(物理内存page偏移地址)).

  • 特别的,xv6中页表的实现是多级页表的形式,一个虚拟地址翻译为物理地址,需要内存管理单元(MMU)访问三次内存(每次访问一个page从对应位置获取下一个待访问page的地址)(页表缓存TLB会将虚拟地址到物理地址的映射记录到缓存中提升效率(处理器内部机制)),其中虚拟地址的27位(拆分成3个9位,分别指定MMU去page的哪个位置寻找下一个待访问page的地址)。
  • 如果将2 ^ 39的虚拟地址全部映射到物理地址,页表的大小为1+512+512*512 = (1 + 2 ^ 9 +2^18)个page,相比直接一级映射节省空间,同时在实际应用中页表是按需构建,即哪些虚拟地址需要映射,才会增加变动页表大小。
    在这里插入图片描述

二、内核页表

内核页表PHYSTOP以下虚拟地址和物理地址是一一对应的,顶端的Trampoline以及每个进程的内核栈(kalloc)不是一一对应的。
boot ROM:当你对主板上电,主板做的第一件事情就是运行存储在boot ROM中的代码,当boot完成之后,会跳转到地址0x80000000,操作系统需要确保那个地址有一些数据能够接着启动操作系统.
物理内存大小(RAM):0x80000000到0x86400000,这个地址范围是100M,不过内核代码中PHYSTOP为0x80000000+12810241024,这个是有128M.(底下的图片是真实开发版的数据,而代码中的地址是qemu软件模拟的地址)
在这里插入图片描述
其中Kernel text和Kernel data先是直接映射到物理内存,另外其中的Trampoline以及进程的内核栈在虚拟地址顶部再重新映射了一次。(即两个虚拟地址映射到同一个物理地址,程序实际运行只用了顶部的虚拟地址)。(问题:kernel text和Kernel data中为什么有Trampoline和进程内核栈?)
答:Trampoline跳转页面是一段特定的汇编代码,保存在Kernel text部分(RX可读可执行)。进程的内核栈是通过kalloc从RAM中申请的1个page.内核页表中虚拟地址从KERNBASE到PHYSTOP之间全部都是直接映射,进程的内核栈申请到的地址也在这个区间里,内核是可以通过申请到的这个地址直接使用进程的内核栈的。(重新映射的原因:高效的上下文切换;内存保护;确保栈空间的独立性;理解起来麻烦,先不管)

三、进程地址空间和页表

在这里插入图片描述
从图中可以看到argument0-N代表的是argv中的每个参数字符串。address of argument N-0对应的是argv中的每个字符串在stack中的地址,argc为参数个数,return PC for main指明程序返回地址。

RISC-V和x86的区别

x86:复杂指令集(闭源)
RISC-V:精简指令集(开源)
ARM:精简指令集
简单来说:不同指令集有不同的汇编语言,不同处理器与不同的指令集配对,可以处理对应的汇编语言。x86处理器可以处理复杂指令汇编语言,RISC-V处理器可以处理RISC-V的汇编语言。

栈的结构

每调用一个函数,都会产生一个(stack frame),栈是从高地址开始向低地址使用。

  • Return address总是会出现在Stack Frame的第一位
  • 指向前一个Stack Frame的指针也会出现在栈中的固定位置(即Return address底下的位置)
  • sp寄存器:指向Stack的底部并代表了当前Stack Frame的位置
  • fp寄存器:指向当前Stack Frame的顶部,可以用来获取Return address以及指向上一个stack frame的指针

在这里插入图片描述

Trap机制:用户空间到内核空间的转换

  • 程序执行系统调用
  • 程序出现了类似page fault、运算时除以0的错误
  • 一个设备触发了中断使得当前程序运行需要响应内核设备驱动
    相关寄存器:
  • STVEC:指向内核中处理trap的指令的起始地址(确保代码执行跳到trampoline位置)
  • SEPC:在trap的过程中保存程序计数器的值(保存用户程序执行的位置)
  • SSRATCH:保存了trapframe的虚拟地址
    trap从用户空间到内核空间的大概流程:
    ECALL指令执行:将程序寄存器的数值保存到SPEC中(保存用户程序的执行位置);将STVEC寄存器中的地址加载到程序寄存器(代码执行位置跳到了trampoline);
    uservec:交换a0寄存器和SSRATCH寄存器的值,此时trapframe的虚拟地址放到了a0寄存器中,此时把剩下保存用户程序执行的寄存器中的值保存到用户进程trapframe对应的内存区域。切换用户页表到内核页表,将sp寄存器指向对应进程的内核栈,接下来内核代码的运行将在进程对应的内核栈中运行。跳转到usertrap.
    usertrap:确定用户空间跳转到内核空间的原因(页面中断,系统调用,外部设备中断等等),并进行处理。
    usertrapret:细节不是那么重要了,遇到问题回去看代码就懂了。
    userret:程序计数器恢复为用户程序的程序计数器,继续执行最初的用户程序。

页面中断47

相关寄存器:
STVAL:保存出错的虚拟地址,或者是触发page fault的源。
SCAUSE :保存发生trap的原因
页面中断主要用到的原因代码为13(load读取)和15(store 写入)
在这里插入图片描述
懒惰页面分配:用户进程申请内存时,先只增加可用空间大小,而不进行映射。这使得进程以为自己有了可用空间并对这些空间进行读写操作,但是由于没有进行内存映射,所以会发生页面中断,此时再对需要读写的页面进行映射,这样可以提高物理内存的空间利用率。不过,由于页面中断涉及用户空间和内核空间的切换,是有成本的。
Copy On Write Fork 在父进程中创建子进程,这里会涉及对父进程的内存的复制,Copy On Write Fork方案是创建的子进程的虚拟内存映射到和父进程一样的物理内存(子进程与父进程共享物理内存),仅当子进程需要访问修改共享物理内存时,为子进程申请独立的页面。这样子可以节省重复从堆区申请大量冗余的页面。
内存映射:为了降低外接存储设备的访问频率,可以将打开的文件的内存映射到物理内存的某一区域,此时可以通过访问物理内存来读取或修改原文件中的信息。

文件系统

xv6文件系统布局(存储在硬盘中)
1个block:1024字节
block0: 无用或者用于boot sector(启动操作系统)
block1: super block, 它描述了文件系统。它可能包含磁盘上有多少个block共同构成了文件系统这样的信息。
block2 - block31: log信息
block32 - block45: innode信息(单个innode是64字节)一个block可以存储16个innode
block46: bitmap block,记录对应位置data block是否空闲
其余block: 存储文件内容信息data block
xv6中一个文件的最大大小如何确定?
一个innode是64字节,其中有13*4 = 52个字节(13个block编号,每个编号占4个字节32位,即xv6最大可以索引2^32个block)指出了文件内容存储在哪个data block,其中前12个block编号的数值直接对应的就是存储文件数据的block,最后一个block是一个一级间接块(indirect block),这个block指向的块中又可以存储256个块号。即一个文件最大大小是(12+256)即268个data block.
在这里插入图片描述

硬盘布局:
[ boot block | super block | log | inode blocks | free bit map | data blocks]
相关代码:xv6中只有1000个block

// super block describes the disk layout:
struct superblock {uint magic;        // Must be FSMAGICuint size;         // Size of file system image (blocks)uint nblocks;      // Number of data blocksuint ninodes;      // Number of inodes.uint nlog;         // Number of log blocksuint logstart;     // Block number of first log blockuint inodestart;   // Block number of first inode blockuint bmapstart;    // Block number of first free map block
};
// On-disk inode structure
struct dinode {short type;           // File type short major;          // Major device number (T_DEVICE only)short minor;          // Minor device number (T_DEVICE only)short nlink;          // Number of links to inode in file system uint size;            // Size of file (bytes) uint addrs[NDIRECT+2];   // Data block addresses 
};

对xv6文件系统的理解:
从用户端理解
文件名构成:根目录/子目录/当前目录/当前文件名。
dir和file 在底层都是一个数据文件,一个数据文件的信息由索引节点标识和内部数据组成,索引节点标识中给出了数据在硬盘中的存储区域,数据量大小,数据文件类型(dir还是file).dir数据文件中存储的是下一级dir和下一级file(由对应的Inode number(索引节点号码)和name组成),file数据文件中保存的是文本或代码。
打开文件过程中,会从根目录数据文件中通过搜索子目录名获取到子目录对应的inode number,接着根据inode number获取到子目录的数据文件,再通过该数据文件获取到当前目录的inode number…,进而得到当前文件名对应得innode number,就可以获取到当前文件的信息了,这就是文件系统读取一个文件中信息的过程。
硬链接:硬链接是指在硬盘中,一个数据文件对应一个 inode(索引节点)。在日常使用中,我们可能希望不同的文件名指向同一个数据文件。例如,当我们复制并重命名一个文件时,操作系统并不会为新文件申请一个新的 inode 或在硬盘上创建新的数据区域。其底层实现过程如下:首先获取旧文件的 inode,然后找到新文件名对应目录项的 inode。接着,将旧文件的 inode number(即 inode 索引号)和新文件的名称写入新文件目录项的数据文件中。这样,在新文件的目录项中新增了一条 entry,包含 inode number 和文件名,从而实现了硬链接。通过这种方式,两个文件名指向同一数据文件,共享相同的 inode。
软链接:软链接则是创建一个新的文件名,并为其分配一个新的 inode。在这个新的 inode 中,存储的是旧文件的路径(即文件名)。软链接本质上是一个指向旧文件的路径引用,而不是直接指向数据块。因此,软链接文件可以指向任何文件,包括不存在的文件,而硬链接只能指向已存在的文件,并且硬链接之间不会区分原文件和链接文件。
硬链接:多个文件名(目录项)指向同一个数据块,文件内容共享。
软链接:新文件名指向原文件的路径,可以跨文件系统,且可以指向不存在的文件。
缓存区高速缓存:频繁从磁盘中读取数据的过程非常慢,设定高速缓存区可以将磁盘中的某一个block对应到一个buffer空间中,在buffer中读写数据完成后再写入磁盘,有效提高程序执行效率。
日志:防止特殊情况,比如断电等特殊情况,使得写入磁盘的过程出现中断,可能会使得inode节点的信息与数据块的信息不对应,造成文件存储错误。日志的简单流程就是先将要写入的数据写入到log block中,等到所有的文件数据写完之后,在日志中标记写入完成,之后再将要写入inode block和data block中的数据从log block中迁移过去。(细节有需要的时候再去扣,现在没有必要)

在这里插入图片描述
在这里插入图片描述

内存映射实验

实现mmap函数(内存映射)和munmap(解除映射)函数
案例:mmap用于将一个内存映射到进程的虚拟地址空间中,通过mmap,程序可以在内存中直接访问文件的内容,而无需使用传统的 read 或 write 系统调用来逐字节地进行文件操作。

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

在xv6中的底层实现:
懒加载:文件映射到进程虚拟地址空间中,最开始只会将进程虚拟地址空间增加length,但是这length空间并没有在进程页表中映射。当进程访问(读取或写入)这部分地址空间时才会利用页面中断机制,对这部分内存进行映射。
解除映射过程中,如果修改在进程地址空间中修改过的内容需要写回文件,在解除映射的时候将虚拟内存中的内容写入到文件当中去。
这样就实现了先将文件中的内容按需映射到进程虚拟地址空间中,然后在进程的虚拟地址空间中对文件内容进行读取更改,解除映射的时候再将文件内容写回到文件当中去(写回硬盘)。这样就避免了频繁反复从硬盘读取写入内容,可以由更好的性能。

Lock实验

临界条件:critical section
1.解决一个锁争用问题:
问题背景:多个进程在不断申请释放内存,这使得内核需要不断调用kalloc以及kfree函数,由于管理物理内存释放调用的链表用一个自旋锁进行保护,当不同CPU中的进程在一个时间段都要访问内存管理链表时,会发生锁争用等待,降低性能。
解决方案:将所有可申请的内存空间分配给不同的CPU,每个CPU针对一部分空间使用一个自旋锁进行保护,这样就可以每个CPU的进程申请释放内存互不干扰,更好提升性能。
2.对于buffer cache的理解:
操作系统缓存区命中:指在操作系统的缓存机制中,所请求的数据已经存在于缓存区内,操作系统能够直接从缓存中获取数据,而无需再次访问底层的存储设备(如硬盘、网络等)。缓存区命中是缓存系统高效工作的一个重要指标,能够显著提高数据访问速度和系统性能。
xv6中buffer cache是用一个双向链表连接了一组buffer数据块。当进程要从硬盘中读取一个block,并不是直接从硬盘读写,而是先为这个block在内存中申请一个buffer缓冲块,将硬盘中的内容读取到buffer数据块中,数据会暂时保存在这个数据块中。如果进程频繁读取就不用从硬盘中读取而是直接从缓冲块中读取,提高了性能,写入也是先写入缓存块再在合适的时间将所有数据同一写回硬盘,提高了性能。

struct logheader {int n;int block[LOGSIZE];
};
日志块中有一个block数组记录了日志块中保留的数据副本最后要写入到哪个数据块中

哪些情况要用锁?
一段内存共享,不同的进程同一时间进行读写;一段代码要具备原子性,如果被不同进程交错执行,会引发错误。
加锁限制了并发性,也限制了性能。
在这里插入图片描述
死锁情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
自旋锁的实现
__sync_lock_test_and_set是一个原子操作,通过向锁的标志位写入1,并获取锁之前的标志位。如果之前的标志位为0,则现在成功获取了锁,如果之前的标志为1,则锁被占用,进入循环。原子操作为了防止一个进程释放锁,另外两个进程同时发现锁被释放,此时会发生两个CPU进程同时获取锁,导致错误。原子操作确保了一次只能有一个CPU进程访问锁的标志位。

void
acquire(struct spinlock *lk)
{push_off(); // disable interrupts to avoid deadlock.if(holding(lk))panic("acquire");// On RISC-V, sync_lock_test_and_set turns into an atomic swap://   a5 = 1//   s1 = &lk->locked//   amoswap.w.aq a5, a5, (s1)while(__sync_lock_test_and_set(&lk->locked, 1) != 0);// Tell the C compiler and the processor to not move loads or stores// past this point, to ensure that the critical section's memory// references happen strictly after the lock is acquired.// On RISC-V, this emits a fence instruction.__sync_synchronize();// Record info about lock acquisition for holding() and debugging.lk->cpu = mycpu();
}

xv6多核运行的理解,进程,线程调度

整体理解:
1.定时器中断,会使用户进程A从用户空间进入到内核空间kernel,此时程序函数调用执行在进程的内核栈,这里也可以说是正在执行内核线程。
2.从内核线程A通过swtch函数跳到调度器线程,这个过程中将进程A内核线程的相关寄存器保存到进程的context中,并从调度器线程即cpu的context取出数据到相关寄存器,此时程序就进入到了scheduler()函数中(死循环寻找状态为runable的进程),找到可以运行的进程B,通过swtch函数从调度器线程跳入到进程B的内核线程,这个过程将调度器线程的寄存器存入到cpu的context中,然后将进程B的context中的数据取出到相关寄存器,此时程序就执行到了进程B的内核线程位置(这个位置也是之前进程B发生调度的位置),之后从内核空间返回到用户空间中执行进程B在用户空间的程序。
3.多核cpu是指多个处理器并行运行,执行相同的代码。不同cpu执行的区别在于,不同cpu调度器线程使用的stack是不同的。程序中会通过从cpu的tp寄存器中获取cpuid,来决定不同cpu执行哪部分代码。
4.在内核空间之所以使用了进程A的内核线程,进程B的内核线程,调度器线程。是因为线程使用的内存空间是一致共用的,区别仅仅是不同线程执行程序的位置(保留在context中)不同,不同线程执行程序调用的堆栈地址不同。而不同进程的用户空间是不共享的,具有独立的虚拟页表,独立申请释放可用空间。
5.xv6中在进程的用户空间,并没有实现线程,按照我的理解,实现线程的步骤大概是:就是为每个线程设定独立的栈空间,设定独立的context存储相关寄存器(ra,sp等等),实现一个线程swtch函数实现不同线程的寄存器切换。这样就可以在不同线程之间切换。
在这里插入图片描述

1.每个cpu都会运行下方这三段程序,多核运行就是并行运行。
2.每个cpu都会有自己的cpuid,每个cpu通过查看自己的tp寄存器(tp寄存器在cpu内部,用作cpu标识)就可以知道自己的编号,同时也就知道了初始化main函数中哪部分程序自己不能运行。
3.初始化部分,为每个CPU设置了调度器线程,以及调度器线程运行的栈空间,调度器线程是进程调度之间的重要一环。(线程的标志就是有自己独立的context,记录自己的栈空间,函数返回地址,以及相关其他寄存器数据,这些决定了线程运行到了哪个位置,等再次调度时可以继续运行,同时不同线程是共享内存空间,内核中的线程共享内核页表对应的空间,用户进程中的线程共享用户进程的用户页表空间)

//汇编语言写成的启动程序# qemu -kernel loads the kernel at 0x80000000        //qemu内核将xv6内核加载到物理地址为0x80000000的内存中,地址范围0x0:0x80000000包含I/O设备# and causes each hart (i.e. CPU) to jump there.     //每个 RISC-V 核心(Hart,代表一个处理器)会跳转到 0x80000000 地址,开始执行内核代码。# kernel.ld causes the following code to             //kernel.ld 是链接器脚本,指定了内核代码将被放置在物理地址 0x80000000 处。# be placed at 0x80000000.
.section .text //指定接下来的代码是文本段(程序代码)
.global _entry // 声明 _entry 为全局符号,意味着它是程序的入口点,供链接器或加载器识别。
_entry: //代码的入口标签# set up a stack for C.     //设置一个栈区运行C代码# stack0 is declared in start.c, # with a 4096-byte stack per CPU.# sp = stack0 + (hartid * 4096)//stack0 在 start.c 文件中声明,为每个 CPU(每个 Hart)分配了 4096 字节的栈空间。这里根据 hartid(每个 CPU 的 ID)来计算每个核心的栈空间。la sp, stack0li a0, 1024*4 // 将 a0 寄存器设置为 4096csrr a1, mhartid //将当前 CPU 的 ID(mhartid)读取到寄存器 a1 中。mhartid 是 RISC-V 的一个控制寄存器,表示当前核心的编号addi a1, a1, 1   //将 a1 寄存器中的值加 1,用来计算每个 CPU 的栈地址偏移(每个 CPU 的栈空间是独立的)mul a0, a0, a1   add sp, sp, a0# jump to start() in start.ccall start
spin:j spin
void
start()
{// set M Previous Privilege mode to Supervisor, for mret.unsigned long x = r_mstatus();x &= ~MSTATUS_MPP_MASK;x |= MSTATUS_MPP_S;w_mstatus(x);// set M Exception Program Counter to main, for mret.// requires gcc -mcmodel=medanyw_mepc((uint64)main);// disable paging for now.w_satp(0);//禁用虚拟页表// delegate all interrupts and exceptions to supervisor mode.w_medeleg(0xffff);w_mideleg(0xffff);w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);// configure Physical Memory Protection to give supervisor mode// access to all of physical memory.w_pmpaddr0(0x3fffffffffffffull);w_pmpcfg0(0xf);// ask for clock interrupts.timerinit();// keep each CPU's hartid in its tp register, for cpuid().int id = r_mhartid();w_tp(id);//mhartid寄存器读取较麻烦,tp寄存器读取较方便// switch to supervisor mode and jump to main().asm volatile("mret");
}
volatile static int started = 0;// start() jumps here in supervisor mode on all CPUs.
void
main()
{if(cpuid() == 0){consoleinit();printfinit();printf("\n");printf("xv6 kernel is booting\n");printf("\n");kinit();         // physical page allocatorkvminit();       // create kernel page table 创建内核页表(完成了设备部分和RAM部分的直接映射以及虚拟地址顶部的Trampoline以及进程内核栈的映射)kvminithart();   // turn on paging 安装内核页表,它将根页表页的物理地址写入寄存器satp。之后,CPU将使用内核页表转换地址。由于内核使用标识映射,下一条指令的当前虚拟地址将映射到正确的物理内存地址。procinit();      // process table 每个进程分配一个内核栈。它将每个栈映射到KSTACK生成的虚拟地址,这为无效的栈保护页面留下了空间。trapinit();      // trap vectorstrapinithart();  // install kernel trap vectorplicinit();      // set up interrupt controllerplicinithart();  // ask PLIC for device interruptsbinit();         // buffer cacheiinit();         // inode tablefileinit();      // file tablevirtio_disk_init(); // emulated hard diskuserinit();      // first user process__sync_synchronize();started = 1;} else {while(started == 0);__sync_synchronize();printf("hart %d starting\n", cpuid());kvminithart();    // turn on paging kvmmap将映射的PTE添加到内核页表中,对kvminithart的调用将内核页表重新加载到satp中,以便硬件知道新的PTE。trapinithart();   // install kernel trap vectorplicinithart();   // ask PLIC for device interrupts}scheduler();        
}

从上面的代码可以看出,每个cpu执行到main函数最后都会执行下方的 scheduler()函数,这里面是一个死循环,不断进行进程调用。每个cpu结构体中proc代表了在当前cpu中执行的进程,context存储了cpu调度器进程切换到其他进程内核线程前的寄存器(包括调度器线程的ra,sp等等),因为一段时间后程序还要回来进行调度其他进程的。

// Per-CPU state.
struct cpu {struct proc *proc;          // The process running on this cpu, or null.struct context context;     // swtch() here to enter scheduler().int noff;                   // Depth of push_off() nesting.int intena;                 // Were interrupts enabled before push_off()?
};
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
void
scheduler(void)
{struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){// The most recent process to run may have had interrupts// turned off; enable them to avoid a deadlock if all// processes are waiting.intr_on();int found = 0;for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);if(p->state == RUNNABLE) {// Switch to chosen process.  It is the process's job// to release its lock and then reacquire it// before jumping back to us.p->state = RUNNING;c->proc = p;swtch(&c->context, &p->context);// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0;found = 1;}release(&p->lock);}if(found == 0) {// nothing to run; stop running on this core until an interrupt.intr_on();asm volatile("wfi");}}
}

swtch函数:从当前线程转到另外的线程中执行。在xv6中主要涉及:从进程内核线程跳转到cpu调度器线程;从cpu调度器线程跳转到其他进程的内核线程。

# Context switch
#
#   void swtch(struct context *old, struct context *new);
# 
# Save current registers in old. Load from new.	
.globl swtch
swtch:sd ra, 0(a0)sd sp, 8(a0)sd s0, 16(a0)sd s1, 24(a0)sd s2, 32(a0)sd s3, 40(a0)sd s4, 48(a0)sd s5, 56(a0)sd s6, 64(a0)sd s7, 72(a0)sd s8, 80(a0)sd s9, 88(a0)sd s10, 96(a0)sd s11, 104(a0)ld ra, 0(a1)ld sp, 8(a1)ld s0, 16(a1)ld s1, 24(a1)ld s2, 32(a1)ld s3, 40(a1)ld s4, 48(a1)ld s5, 56(a1)ld s6, 64(a1)ld s7, 72(a1)ld s8, 80(a1)ld s9, 88(a1)ld s10, 96(a1)ld s11, 104(a1)ret

睡眠锁的实现

解决lost-wakeup问题:
这个问题比较复杂,忘记了就再回去看一下教程。
解决条件:

  • 调用sleep时需要持有condition lock,这样sleep函数才能知道相应的锁。(确保持有睡眠锁的一方可以开始释放睡眠锁)
  • sleep函数只有在获取到进程的锁p->lock之后,才能释放condition lock。
  • wakeup需要同时持有两个锁才能查看进程。(wakeup只有当另一个未持有锁的进程设置成sleep状态之后,释放进程锁后菜才能唤醒该进程)
void
acquiresleep(struct sleeplock *lk)
{acquire(&lk->lk);//必须先获取自旋锁while (lk->locked) {sleep(lk, &lk->lk);//在sleep中释放自旋锁,如果不释放,另一个持有锁的进程就无法释放睡眠锁}lk->locked = 1;lk->pid = myproc()->pid;release(&lk->lk);
}
void
releasesleep(struct sleeplock *lk)
{acquire(&lk->lk);//另一个进程在sleep中释放了自旋锁,这里才能获取lk->locked = 0;lk->pid = 0;wakeup(lk);release(&lk->lk);
}
void
void
sleep(void *chan, struct spinlock *lk)
{struct proc *p = myproc();// Must acquire p->lock in order to// change p->state and then call sched.// Once we hold p->lock, we can be// guaranteed that we won't miss any wakeup// (wakeup locks p->lock),// so it's okay to release lk.acquire(&p->lock);  //DOC: sleeplock1release(lk);// Go to sleep.p->chan = chan;p->state = SLEEPING;sched();// Tidy up.p->chan = 0;// Reacquire original lock.release(&p->lock);acquire(lk);
}
wakeup(void *chan)
{struct proc *p;for(p = proc; p < &proc[NPROC]; p++) {if(p != myproc()){acquire(&p->lock);if(p->state == SLEEPING && p->chan == chan) {//查看是不是这个进程在睡眠等待事件发生p->state = RUNNABLE;}release(&p->lock);}}
}

如何理解父进程与子进程的执行

1.在用户空间中,一个进程(father)调用fork函数创建子进程(son),子进程会完全复制父进程的一切,包括页表内容,trapframe中的内容,stack中的内容,堆区的内容,代码段,数据段的内容。即子进程可以看作是一个父进程的副本
2.father进程调用fork系统调用进入内核空间,将用户空间运行的相关的寄存器全部保存到了trapframe中,而son进程则完全复制了trapframe中的内容,也就是说当son进程在内核中被调度并返回到son进程用户空间时,程序执行的位置和father进程程序执行的位置是一样的,即father进程和son进程在用户空间都运行到了pid = fork();这段代码。
3.唯一不同的点是fork函数中对子进程中np->trapframe->a0的值赋值为0,而np->trapframe->a0的值对应的则是内核调用的返回值,即pid的值,此时father进程的pid的值为大于0的pid数值,而子进程的pid数值为0.通过这一点就能够很好的区分父进程执行哪一段代码,子进程执行哪一段代码。对于没有pid判断语句的代码区域,是父进程和子进程都会执行的区域。
4.这就解释了在用户空间中pid判断语句的代码。
5.一个进程要释放,先运行exit,再运行wait,wait函数中才是真正释放进程页表等相关内容,exit只是释放了相关文件引用,并将进程状态设置为ZOMBIE,wait函数检测到ZOMBIE只会则会回收该进程的一切。(其中也用到了sleep_wakeup机制)

int pid = fork();
if(pid > 0) {printf("parent: child=%d\n", pid);pid = wait((int *) 0);printf("child %d is done\n", pid);
} else if(pid == 0) {printf("child: exiting\n");exit(0);
} else {printf("fork error\n");
}
int
fork(void)
{int i, pid;struct proc *np;struct proc *p = myproc();//获取父进程信息// Allocate process.//分配进程if((np = allocproc()) == 0){return -1;//内存申请失败}// Copy user memory from parent to child.//复制父进程页表信息到子进程中,父子进程公用页表信息if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){freeproc(np);release(&np->lock);return -1;}np->sz = p->sz;// copy saved user registers.*(np->trapframe) = *(p->trapframe);//公用寄存器信息// Cause fork to return 0 in the child.np->trapframe->a0 = 0; //a0寄存器为0// increment reference counts on open file descriptors. //文件描述符计数for(i = 0; i < NOFILE; i++)if(p->ofile[i])np->ofile[i] = filedup(p->ofile[i]);//子进程复制父进程文件句柄np->cwd = idup(p->cwd);//复制当前目录safestrcpy(np->name, p->name, sizeof(p->name));//添加一句话将跟踪掩码从父进程复制到子进程np->tracemask = p->tracemask;pid = np->pid;release(&np->lock);acquire(&wait_lock);np->parent = p;release(&wait_lock);acquire(&np->lock);np->state = RUNNABLE;release(&np->lock);return pid;
}

中断

PLIC会管理外设中断。会先通知所有cpu发生了中断,只会有一个cpu接收中断,之后PLIC就会将中断的具体情况发送给对应CPU,如果当前所有CPU都在处理中断,那么PLIC会暂时将中断信息保留下来,等到有CPU空闲时才会处理中断。CPU核处理完中断之后,CPU会通知PLIC,PLIC将不再保存中断的信息。
在这里插入图片描述

进程结束

exit(0)只是关闭了进程的相关文件fd,并将进程的状态修改为ZOMBE
wait(0)是用来释放进程的内存的。
释放进程exit和wait是需要同时调用的,一个都少不来。
exit的传入参数指出了当前进程结束是正常结束0还是说异常计数-1
wait(&status)可以将exit的传入参数保存到变量status中去,这样就知道进程是如何结束掉的了,可以用来debug.这下搞清楚了。wait传入0参数表明不关心进程是如何结束的。主动调用exit(0)是一个比较好的习惯。C语言当程序执行完之后也会默认自动调用exit关闭相关文件描述符。子进程的close(fd)不管也行的,但是为了维护者方便,子进程中额外使用的文件描述符最好使用close(fd),方便维护者理解代码。

相关文章:

操作系统理解(xv6)

xv6操作系统项目复习笔记 宗旨&#xff1a;只记大框架&#xff0c;不记细节&#xff0c;没有那么多的时间 一、xv6的页表是如何搭建的&#xff1f; xv6这个项目中&#xff0c;虚拟地址用了39位&#xff08;27位12位&#xff08;物理内存page偏移地址&#xff09;&#xff09…...

2024CCPC辽宁省赛 个人补题 ABCEGJL

Dashboard - 2024 CCPC Liaoning Provincial Contest - Codeforces 过题难度 B A J C L E G 铜奖 4 953 银奖 6 991 金奖 8 1664 B&#xff1a; 模拟题 // Code Start Here string s;cin >> s;reverse(all(s));cout << s << endl;A&#xff1a;很…...

Sentinel原理与SpringBoot整合实战

前言 随着微服务架构的广泛应用&#xff0c;服务和服务之间的稳定性变得越来越重要。在高并发场景下&#xff0c;如何保障服务的稳定性和可用性成为了一个关键问题。阿里巴巴开源的Sentinel作为一个面向分布式服务架构的流量控制组件&#xff0c;提供了从流量控制、熔断降级、…...

Python 训练营打卡 Day 31

文件的规范拆分和写法 把一个文件&#xff0c;拆分成多个具有着独立功能的文件&#xff0c;然后通过import的方式&#xff0c;来调用这些文件。这样具有几个好处&#xff1a; 可以让项目文件变得更加规范和清晰可以让项目文件更加容易维护&#xff0c;修改某一个功能的时候&a…...

vue+srpingboot实现多文件导出

项目场景&#xff1a; vuesrpingboot实现多文件导出 解决方案&#xff1a; 直接上干货 <el-button type"warning" icon"el-icon-download" size"mini" class"no-margin" click"exportSelectedFiles" :disabled"se…...

学习 Pinia 状态管理【Plan - May - Week 2】

一、定义 Store Store 由 defineStore() 定义&#xff0c;它的第一个参数要求独一无二的id import { defineStore } from piniaexport const useAlertsStore defineStore(alert, {// 配置 })最好使用以 use 开头且以 Store 结尾 (比如 useUserStore&#xff0c;useCartStore&a…...

linux中cpu内存浮动占用,C++文件占用cpu内存、定时任务不运行报错(root) PAM ERROR (Permission denied)

文章目录 说明部署文件准备脚本准备部署g++和编译脚本使用说明和测试脚本批量部署脚本说明执行测试定时任务不运行报错(root) PAM ERROR (Permission denied)报错说明处理方案说明 我前面已经弄了几个版本的cpu和内存占用脚本了,但因为都是固定值,所以现在重新弄个用C++编写的…...

数据湖和数据仓库的区别

在当今数据驱动的时代&#xff0c;企业需要处理和存储海量数据。数据湖与数据仓库作为两种主要的数据存储解决方案&#xff0c;各自有其独特的优势与适用场景。本文将客观详细地介绍数据湖与数据仓库的基本概念、核心区别、应用场景以及未来发展趋势&#xff0c;帮助读者更好地…...

OceanBase 开发者大会,拥抱 Data*AI 战略,构建 AI 数据底座

5 月 17 号以“当 SQL 遇见 AI”为主题的 OceanBase 开发者大会在广州举行&#xff0c;因为行程的原因未能现场参会&#xff0c;仍然通过视频直播观看了全部的演讲。总体来说&#xff0c;这届大会既有对未来数据库演进方向的展望&#xff0c;也有 OceanBase 新产品的发布&#…...

鸿蒙HarmonyOS最新的组件间通信的装饰器与状态组件详解

本文系统梳理了鸿蒙&#xff08;HarmonyOS&#xff09;ArkUI中组件间通信相关的装饰器及状态组件的使用方法&#xff0c;重点介绍V2新特性&#xff0c;适合开发者查阅与实践。 概述 鸿蒙系统&#xff08;HarmonyOS&#xff09;ArkUI提供了丰富的装饰器和状态组件&#xff0c;用…...

OneDrive登录,账号跳转问题

你的OneDrive登录无需密码且自动跳转到其他账号&#xff0c;可能是由于浏览器或系统缓存了登录信息&#xff0c;或存在多个账号的关联。以下是分步解决方案&#xff1a; 方案三对我有效。 强制手动输入密码 访问登录页面时&#xff1a; 在浏览器中打开 OneDrive网页版。 点击…...

9-码蹄集600题基础python篇

题目如上图所示。 这一题&#xff0c;没什么难度。 代码如下&#xff1a; def main():#code here# x,amap(int,input("").split(" "))# sum((1/2)*(a*x(ax)/(4*a)))# print(f"{sum:.2f}")x,amap(int,input().split())print(f"{((1/2)*(a*…...

CAU人工智能class3 优化器

优化算法框架 优化思路 随机梯度下降 随机梯度下降到缺点&#xff1a; SGD 每一次迭代计算 mini-batch 的梯度&#xff0c;然后对参数进行更新&#xff0c;每次迭代更新使用的梯度都只与本次迭代的样本有关。 因为每个批次的数据含有抽样误差&#xff0c;每次更新可能并不会 …...

学习 Android(十一)Service

简介 在 Android 中&#xff0c;Service 是一种无界面的组件&#xff0c;用于在后台执行长期运行或跨进程的任务&#xff0c;如播放音乐、网络下载或与远程服务通信 。Service 可分为“启动型&#xff08;Started&#xff09;”和“绑定型&#xff08;Bound&#xff09;”两大…...

SpringAI开发SSE传输协议的MCP Server

SpringAI 访问地址&#xff1a;Spring AI ‌ Spring AI‌是一个面向人工智能工程的应用框架&#xff0c;由Spring团队推出&#xff0c;旨在将AI能力集成到Java应用中。Spring AI的核心是解决AI集成的根本挑战&#xff0c;即将企业数据和API与AI模型连接起来‌。 MCP…...

【泛微系统】后端开发Action常用方法

后端开发Action常用方法 代码实例经验分享:代码实例 经验分享: 本文分享了后端开发中处理工作流Action的常用方法,主要包含以下内容:1) 获取工作流基础信息,如流程ID、节点ID、表单ID等;2) 操作请求信息,包括请求紧急程度、操作类型、用户信息等;3) 表单数据处理,展示…...

如何成为更好的自己?

成为更好的自己是一个持续成长的过程&#xff0c;需要结合自我认知、目标规划和行动力。以下是一些具体建议&#xff0c;帮助你逐步提升&#xff1a; 1. 自我觉察&#xff1a;认识自己 反思与复盘&#xff1a;每天花10分钟记录当天的决策、情绪和行为&#xff0c;分析哪些做得…...

精益数据分析(74/126):从愿景到落地的精益开发路径——Rally的全流程管理实践

精益数据分析&#xff08;74/126&#xff09;&#xff1a;从愿景到落地的精益开发路径——Rally的全流程管理实践 在创业的黏性阶段&#xff0c;如何将抽象的愿景转化为可落地的产品功能&#xff1f;如何在快速迭代中保持战略聚焦&#xff1f;今天&#xff0c;我们通过Rally软…...

网站制作公司哪家强?(2025最新版)

在数字化时代&#xff0c;一个优质的网站是企业展示自身实力、拓展业务渠道的重要工具。无论是初创企业还是大型集团&#xff0c;都需要一个功能强大、设计精美的网站来吸引客户、提升品牌形象。但面对市场上众多的网站制作公司&#xff0c;如何选择一家靠谱的合作伙伴呢&#…...

23种经典设计模式(GoF设计模式)

目录 &#x1f340; 创建型设计模式&#xff08;5种&#xff09; 1. 单例模式&#xff08;Singleton&#xff09; 2. 工厂方法模式&#xff08;Factory Method&#xff09; 3. 抽象工厂模式&#xff08;Abstract Factory&#xff09; 4. 建造者模式&#xff08;Builder&am…...

深入解析Dify:从架构到应用的全面探索

文章目录 引言一、Dify基础架构1.1 架构概述1.2 前端界面1.3 后端服务1.4 数据库设计 二、Dify核心概念2.1 节点&#xff08;Node&#xff09;2.2 变量&#xff08;Variable&#xff09;2.3 工作流类型 三、代码示例3.1 蓝图注册3.2 节点运行逻辑3.3 工作流运行逻辑 四、应用场…...

电子电路:怎么理解放大电路中集电极电流Ic漂移?

如果放大电路中集电极电阻RC因为温度或老化而阻值变化&#xff0c;Vce Vcc - IcRc - IcRc&#xff0c;这会改变工作点&#xff0c;导致集电极的电流漂移。 IC漂移的定义&#xff1a;集电极电流随时间、温度等变化。影响IC的因素&#xff1a;β、IB、VBE、温度、电源电压、元件…...

【疑难杂症】Mysql 无报错 修改配置文件后服务启动不起来 已解决|设置远程连接

我修改配置后&#xff0c;服务无法启动可以试试用记事本打开后另存为&#xff0c;格式选择ANSI&#xff0c;然后重新启动mysql试试 设置运行远程、 1、配置my.ini文件 在[mysqld]下 添加bind-address0.0.0.0 2、设置root权限 使用MySql命令行执行&#xff0c; CREATE USER…...

Java基础 5.21

1.多态注意事项和细节讨论 多态的前提是&#xff1a;两个对象&#xff08;类&#xff09;存在继承关系 多态的向上转型 本质&#xff1a;父类的引用指向了子类的对象语法&#xff1a;父类类型 引用名 new 子类类型();特点&#xff1a;编译类型看左边&#xff0c;运行类型看…...

探索Puter:一个基于Web的轻量级“云操作系统”

在云计算与Web技术高度融合的今天,开发者们不断尝试将传统桌面体验迁移到浏览器中。近期,GitHub上一个名为Puter的开源项目吸引了社区的关注。本文将带你深入解析Puter的设计理念、技术架构与使用场景,探索它如何通过现代Web技术重构用户的“云端桌面”。 一、项目概览 Put…...

Java SpringBoot 项目中 Redis 存储 Session 具体实现步骤

目录 一、添加依赖二、配置 Redis三、配置 RedisTemplate四、创建控制器演示 Session 使用五、启动应用并测试六、总结 Java 在 Spring Boot 项目中使用 Redis 来存储 Session&#xff0c;能够实现 Session 的共享和高可用&#xff0c;特别适用于分布式系统环境。以下是具体的实…...

电商项目-商品微服务-规格参数管理,分类与品牌管理需求分析

本文章介绍&#xff1a;规格参数管理与分类与品牌管理的需求分析和表结构的设计。 一、规格参数管理 规格参数模板是用于管理规格参数的单元。规格是例如颜色、手机运行内存等信息&#xff0c;参数是例如系统&#xff1a;安卓&#xff08;Android&#xff09;后置摄像头像素&…...

Java 定时任务中Cron 表达式与固定频率调度的区别及使用场景

Java 定时任务&#xff1a;Cron 表达式与固定频率调度的区别及使用场景 一、核心概念对比 1. Cron 表达式调度 定义&#xff1a;基于日历时间点的调度&#xff0c;通过 秒 分 时 日 月 周 年 的格式定义复杂时间规则。时间基准&#xff1a;绝对时间点&#xff08;如每天 12:…...

2025年- H39-Lc147 --394.字符串解码(双栈,递归)--Java版

1.题目描述 2.思路 可以用递归也可以用双栈&#xff0c;这边用栈。 首先先创建一个双栈&#xff0c;一个栈存数字&#xff08;interger&#xff09;&#xff0c;另一个栈存字符&#xff08;character&#xff09;。设置数字临时变量num&#xff0c;设置字母临时变量curString在…...

学编程对数学成绩没帮助?

今天听到某机构直播说“学编程对数学成绩没帮助&#xff0c;如果想提高数学成绩那就单独去学数学”&#xff0c;实在忍不住要和各位家长聊聊我的思考&#xff0c;也欢迎各位家长评论。 恰在此时我看见了一道小学6年级的数学题如下&#xff0c;虽然题不难&#xff0c;但立刻让我…...

现代计算机图形学Games101入门笔记(十九)

光场 在近处画上图像&#xff0c;VR的效果。 任何时间任何位置看到的图像都不一样&#xff0c;是不是就是一个世界了。 光场就是任何一个位置往任何一个方向去的光的强度 知道光场就能知道这个物体长什么样子。 光线可以用一个点和一个方向确定。 也可以用2个点确定一条光线。 …...

STM32单片机GUI系统1 GUI基本内容

目录 一、GUI简介 1、emWin 2、LVGL (Light and Versatile Graphics Library) 3、TouchGFX 4、Qt for Embedded 5、特性对比总结 二、LVGL移植要求 三、优化LVGL运行效果方法 四、LVGL系统文件 一、GUI简介 在嵌入式系统中&#xff0c;emWin、LVGL、TouchGFX 和 Qt 是…...

Prometheus+Grafana实现对服务的监控

PrometheusGrafana实现对服务的监控 前言&#xff1a;PrometheusGrafana实现监控会更加全面&#xff0c;监控的组件更多 Prometheus官网 https://prometheus.io/docs/prometheus/latest/getting_started/ Grafana官网 https://grafana.com/docs/ 一、安装PrometheusGrafana 这…...

hook原理和篡改猴编写hook脚本

hook原理&#xff1a; hook是常用于js反编译的技术&#xff1b;翻译就是钩子&#xff0c;他的原理就是劫持js的函数然后进行篡改 一段简单的js代码 &#xff1a;这个代码是顺序执行的 function test01(){console.log(test01)test02() } function test02(){console.log(02)tes…...

Sign签证绕过

Sign的简介 Sign是指一种类似于token的东西 他的出现主要是保证数据的完整性&#xff0c;防篡改 就是一般的逻辑是 sign的加密的值和你输入的数据是相连的&#xff08;比如sign的加密是使用输入的数据的前2位数字配合SHA1 等这样的&#xff09; 绕过 &#xff1a;碰运气可以…...

【Vue篇】重剑无锋:面经PC项目工程化实战面经全解

目录 引言 一、项目功能演示 1. 目标 2. 项目收获 二、项目创建目录初始化 vue-cli 建项目 三、ESlint代码规范及手动修复 1. JavaScript Standard Style 规范说明 2. 代码规范错误 3. 手动修正 四、通过eslint插件来实现自动修正 五、调整初始化目录结构 1. 删除…...

JVM参数详解与实战案例指南(AI)

JVM参数详解与实战案例指南 一、JVM参数概述与分类 JVM参数是控制Java虚拟机运行时行为的关键配置项&#xff0c;合理设置这些参数可以显著提升应用性能。根据功能和稳定性&#xff0c;JVM参数主要分为三类&#xff1a; 标准参数&#xff1a;所有JVM实现都必须支持&#xff…...

C++通过空间配置器实现简易String类

C实现简易String类 在C中&#xff0c;使用空间配置器&#xff08;allocator&#xff09;实现自定义string类需要管理内存分配、释放及对象构造/析构。 #include <memory> #include <algorithm> #include <cstring> #include <stdexcept> #include &l…...

MyBatis:简化数据库操作的持久层框架

1、什么是Mybatis? MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由 apachesoftwarefoundation 迁移到了google code,由谷歌托管,并且改名为MyBatis 。 2013年11月迁移到Github。 iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框…...

Spring Boot集成Spring AI与Milvus实现智能问答系统

在Spring Boot中集成Spring AI与Milvus实现智能问答系统 引言 随着人工智能技术的快速发展&#xff0c;越来越多的企业开始探索如何将AI能力集成到现有系统中。本文将介绍如何在Spring Boot项目中集成Spring AI和向量数据库Milvus&#xff0c;构建一个高效的智能问答系统。 …...

软件工程(六):一致性哈希算法

哈希算法 定义 哈希算法是一种将任意长度的输入&#xff08;如字符串、文件等&#xff09;转换为固定长度输出的算法&#xff0c;这个输出称为“哈希值”或“摘要”。 常见的哈希算法 哈希算法哈希位数特点MD5128位快速&#xff0c;但已不安全SHA-1160位安全性提高&#xf…...

Linux内存分页管理详解

Linux内存分页管理详解:原理、实现与实际应用 目录 Linux内存分页管理详解:原理、实现与实际应用 一、引言 二、内存分页机制概述 1. 虚拟地址与物理地址的划分 2. 分页的基本原理 三、虚拟地址到物理地址的转换 1. 地址转换流程 2. 多级页表的遍历 四、多级页表的…...

work-platform阅读

Redis存储的是字节数据&#xff0c;所以任何对象想要存进redis&#xff0c;都要转化成字节。对象转化为字节流的过程&#xff0c;叫序列化&#xff0c;反之&#xff0c;叫反序列化 Redis 序列化详解及高性能实践-CSDN博客https://blog.csdn.net/zhangkunls/article/details/14…...

在 Excel xll 自动注册操作 中使用东方仙盟软件————仙盟创梦IDE

windows 命令 "C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE" /X "C:\Path\To\仙盟.xll" excel 注册 Application.RegisterXLL "XLMAPI.XLL" 重点代码解析 excel 命令模式 [ExcelCommand(Description "使用参数")] …...

微调后的模型保存与加载

在Hugging Face Transformers库中&#xff0c;微调后的模型保存与加载方式因微调方法&#xff08;如常规微调或参数高效微调&#xff09;而异。 一、常规微调模型的保存与加载 1、 保存完整模型 使用 save_pretrained() 方法可将整个模型&#xff08;包含权重、配置、分词器…...

PostgreSQL 日常维护

目录 一、基本使用 1、登录数据库 2、数据库操作 &#xff08;1&#xff09;列出库 &#xff08;2&#xff09;创建库 &#xff08;3&#xff09;删除库 &#xff08;4&#xff09;切换库 &#xff08;5&#xff09;查看库大小 3、数据表操作 &#xff08;1&#xff…...

Ntfs!ATTRIBUTE_RECORD_HEADER结构$INDEX_ROOT=0x90的一个例子

Ntfs!ATTRIBUTE_RECORD_HEADER结构$INDEX_ROOT0x90的一个例子 1: kd> dx -id 0,0,899a2278 -r1 ((Ntfs!_FILE_RECORD_SEGMENT_HEADER *)0xc431a400) ((Ntfs!_FILE_RECORD_SEGMENT_HEADER *)0xc431a400) : 0xc431a400 [Type: _FILE_RECORD_SEGMENT_HEADER …...

leetcode hot100刷题日记——7.最大子数组和

class Solution { public:int maxSubArray(vector<int>& nums) {//方法一&#xff1a;动态规划//dp[i]表示以i下标结尾的数组的最大子数组和//那么在i0时&#xff0c;dp[0]nums[0]//之后要考虑的就是我们要不要把下一个数加进来&#xff0c;如果下一个数加进来会使结…...

LlamaIndex

1、大语言模型开发框架的价值是什么? SDK:Software Development Kit,它是一组软件工具和资源的集合,旨在帮助开发者创建、测试、部署和维护应用程序或软件。 所有开发框架(SDK)的核心价值,都是降低开发、维护成本。 大语言模型开发框架的价值,是让开发者可以更方便地…...

下一代电子电气架构(EEA)的关键技术

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...