用户态,内核态是什么,什么时候会切换
内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。
用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。
内核态向用户态切换时机:
1.系统调用:就是用户进程主动要求切换到内核态的一种方式,通过操作系统为用户特别开放的一个中断来实现(软中断)
2.异常:当CPU在执行运行在用户态的程序时,发现了某些异常,就会由当前运行进程切换到处理此异常的内核相关程序中,也就到了内核态,比如缺页异常
3.外围设备的中断:当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
虚拟内存有什么作用
广义上虚拟内存就是操作系统的一种内存管理技术,狭义上虚拟内存更像是一个标签,一个title,起到标识或者描述物理内存的作用。因为我们直接操作物理内存的话心智压力比较大,比如你声明一个变量时你得通过物理地址申请内存,对计算机内存的读写操作需要在最底层去实现等等。而虚拟内存机制的存在,操作系统会形成虚拟内存与物理内存之间的映射,可以理解为虚拟内存是操作系统提供给我们的一种接口。
虚拟内存的好处:
1、可以运行内存更大的进程。基于程序的局部性原理,通过swap机制,操作系统会把内存中不常用的内容置换到硬盘中,这样操作系统就可以运行比物理内存大的进程了
2、避免多个进程的内存地址冲突。每个进程的虚拟内存表都是独立的,进程间的页表互不影响。当多个进程申请同一个虚拟内存时,操作系统会把这三虚拟内存转化为对应的三块物理内存去,所以即使是分配内存地址时出现地址相同了也不会出问题。
3、多了一些描述物理内存的一些标识,提高了操作系统的安全性。
malloc()如何分配内存
分配的是虚拟内存。
malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;滥用的话会出现内存碎片的问题
malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。滥用的话会需要频繁的发生运行态的切换,第一次访问虚拟内存还会发生缺页中断,cpu开销大
进程调用mmap,内核做了什么
- 建立进程虚拟地址空间与文件内容空间之间的映射
- 而后第一次读写mmap映射的内存时,由于页表并未与物理内存映射,触发缺页异常
- 缺页异常程序先根据要访问的偏移和大小从page cache中查询是否有该文件的缓存,如果找到就更新进程页表指向page cache那段物理内存
- 没找到就将文件从磁盘加载到内核page cache,然后再令进程的mmap虚拟地址的页表指向这段page cache中文件部分的物理内存
所以结论是,内核会把文件读到page cache中。也只有这样,其它进程打开的文件会和mmap打开的文件读写结果保持一致。
内存满了,会发生什么?
首先程序调用malloc申请的是虚拟内存,不是物理内存。当程序读取申请的虚拟内存,如果没有相映射的物理内存时,cpu发起缺页中断,进程从用户态转为内核态,并将缺页中断交给内核的缺页中断处理函数。缺页中断处理函数会判断当前有无可分配的物理内存,如果有直接分配给虚拟内存形成映射,如果无,则进行内存回收。
内存回收分为:后台内存回收和直接内存回收
后台内存回收:在物理内存紧张的时候,会唤醒kswapd内核线程来进行内存回收,整个过程是异步进行,不会阻塞进程的执行,所以优先进行
直接内存回收:后台异步回收跟不上进程内存申请的速度,就会开启直接内存回收,过程是同步的,会阻塞进程的执行。
如果直接回收之后物理内存还是不够分配,那么会触发OOM机制,OOM killer 会选择杀死占用物理内存高的进程,直到有足够的物理内存空间可以分配。
可回收的内存分为:文件页和匿名页
文件页:内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都是文件页。大部分文件页可以直接回收,需要的时候再从硬盘拿。有一些发生更新但还没写进磁盘的文件(脏页),就得先写进磁盘再进行内存释放。所以,回收干净页是直接回收,回收脏页是先写到硬盘再回收。
匿名页:这部分内容没有实际的载体,不像文件缓存有硬盘文件做载体,比如堆栈。这部分内容很可能被再次访问,所以不能直接释放内存,它们回收的方式通过Linux的Swap机制,将不常用的内存备份到硬盘后,然后释放当前内存。若再次访问的话就从硬盘里读入内存即可
进程、线程、协程
进程:是运行中的程序,资源分配和调度的基本单位
线程:是进程的一个执行单元,程序执行的基本单位
协程:用户态的轻量级线程,线程内部调度的基本单位
区别:
从资源共享方面来说:进程之间相互隔离,不共享资源。线程间共享代码区,数据区,堆区,动态链接库,文件,当前工作目录,用户id和组id,但是从实际情况来看,栈区也是共享的,因为线程的栈区没有严格的隔离机制来保护,如果一个线程能拿到来自另一个线程栈帧上的指针,那么该线程就可以改变另一个线程的栈区。协程之间也会共享资源,但是对于共享什么资源,我没查到过这方面的资料,以我的观点来看,因为协程是线程内部的基本单位,我觉得协程共享的资源就是线程所管理的资源。
从系统开销上来说:进程间的切换涉及到虚拟地址空间,硬件上下文,页表,内核栈这些的切换,开销很大。线程间的切换涉及到内核栈,线程上下文的切换,开销较小。协程间的切换涉及到寄存器内容的切换,开销很小。
从健壮性来看:多进程更加健壮,一个进程崩掉后,在保护模式下,不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
还有进程和线程的调用栈是内核栈,由操作系统进行切换,涉及到用户态和内核态之间的切换,协程的调用栈是用户栈,由用户进行切换,不会陷入内核态
什么是进程的上下文
CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文
进程切换涉及虚拟地址空间的切换而线程不一定会
发生进程上下文切换有哪些场景?
为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
什么是PCB
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
进程描述信息:
进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息:
进程当前状态,如 new、ready、running、waiting 或 blocked 等;
进程优先级:进程抢占 CPU 时的优先级;
资源分配清单:
有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
CPU 相关信息:
CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
进程如何隔离资源
地址空间隔离:每个进程都有自己独立的虚拟地址空间,这意味着不同进程无法直接访问对方的内存。操作系统通过管理页表、虚拟内存等机制来实现进程之间的地址空间隔离。
文件描述符隔离:在Unix/Linux系统中,每个进程都拥有自己独立的文件描述符表。这样不同进程的文件操作就不会相互干扰。
进程间通信(IPC)机制:进程通过IPC机制进行通信和数据交换,而不是直接访问对方的内存或者共享资源。常见的IPC机制包括管道、消息队列、共享内存等。
CPU时间片轮转调度:操作系统使用CPU时间片轮转调度算法,为每个进程分配一个时间片,当时间片用尽后,操作系统将该进程挂起,并切换到下一个可运行的进程。这样不同进程的CPU时间得到保证,而且也避免了某个进程占用CPU时间过长导致其他进程等待的情况。
用户和权限分离:操作系统将用户和权限进行分离,不同用户拥有不同的权限,这样就可以避免某个进程通过提升权限来获取其他进程的数据或者访问其他进程的资源。
进程通信IPC
最简单的就是管道通信,管道分为匿名管道和命名管道。匿名管道只能用于父子进程之间的通信,而命名管道则允许陌生进程之间通信。管道本质就是一段内核的缓存,缓存的数据是无格式的流且大小受限制。管道数据流向是单向的,先进先出,所以双向数据流需要双管道实现。
接着是消息队列,本质是内核维护的一个链表,可以存放任意数据类型,解决了管道数据无格式的问题。进程通过对消息队列的数据的读写实现通信,而读写都需要拷贝消息队列的中的数据,所以效率比较低。
共享内存是基于虚拟内存(危:那你说说虚拟内存)实现的,不同进程对应不同的虚拟内存,而将需要进行通信的进程的虚拟内存映射到同一块物理内存,这块物理内存就是共享内存。直接对内存进行读写,所以效率很高,但会出现进程读写操作冲突的问题。
信号量起计时器的作用,进程之间维护一个信号量表示当前资源的数量,通过PV操作实现进程之间的同步与互斥访问。信号量初始化为0则实现同步,为1则实现进程互斥。
信号是异步通信机制。内核可以通过信号直接和进程交互。信号事件的来源主要有硬件来源(如键盘Cltr+C)和软件来源(如kill命令),一旦有信号发生,进程有三种方式响应信号,执行默认操作,捕获信号以及忽略信号,有两个信号无法捕获和忽略,SIGKILL和SIGSTOP,方便终止或停止进程。
最后就是socket通信,可以实现不同主机之间进程的通信。
线程相比进程能减少开销,体现在:
线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
线程的上下文切换分为两种情况:
1.前后两个线程属于不同进程,此时,由于资源不共享,所以切换过程就跟进程上下文切换是一样的。
2.前后两个线程属于同一个进程,此时,应为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据,寄存器等不共享的数据。
线程独占的资源
-
线程运行的本质就是函数的执行,函数运行时的信息保存在栈帧中,包括函数的返回值、使用的局部变量、寄存器信息等,因此每个进程都有自己独立的、私有的栈区
-
程序计数器、函数运行使用的寄存器组的值也是线程私有的
-
每个线程用户独立的线程ID
线程资源怎么回收
pthread_join函数来回收;
pthread_detach可以讲线程转换为detached状态,子线程运行完成之后可以自行回收资源。
软中断和硬中断
软中断是执行中断指令产生的,而硬中断是由外设引发的。
硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。
硬中断是可屏蔽的,软中断不可屏蔽。
说说程序的局部性原理
局部性原理表现在以下两个方面:
时间局部性:如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,
什么是CPU上下文
CPU上下文:CPU 寄存器和程序计数器
CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)
程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
为什么虚拟地址空间切换会比较耗时
简单的说,一旦去切换进程上下文进而需要切换虚拟地址空间时,处理器的页表缓冲(TLB)会被全部清空,这将导致虚拟内存转换为物理内存(访问内存)在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB。当进程切换后页表也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。
cache缓存数据,减少访问内存的时间消耗,加速读写内存。而tlb是cache的一种,用来缓存页表项,减少查找页表的时间消耗,加速虚拟地址转换为物理地址
内存管理三种方式:
请求分页存储管理。
请求分段存储管理。
请求段页式存储管理。
快表:页表的缓存,存放在高速缓冲存储器的部分页表,提高访问速率
请求分页:将当前需要的一部分页面装入内存,便可以启动作业运行。在作业执行过程中,当所要访问的页面不在内存时,通过调页功能将其调入,同时还可以通过置换功能将暂时不用的页面换出到外存上,以便腾出内存空间。
工作流程:拿着页号去快表中看看是否存在快表中,如果有就加上偏移量找到实际的物理地址执行相应指令即可。
若找不到则先将页号和页表寄存器比较一下,看看是否越界
若没越界则去请求页表,请求成功将页面加载到内存中,并加上偏移量找到对应指令并执行;若页表中没有这个页面则触发缺页中断,将进程塞到阻塞队列中,(CPU)向操作系统发出缺页中断请求,操作系统就查找对应磁盘的页面的位置,将其换入到物理内存中,更新页表,重新执行
注意,若内存没有空闲空间则还会通过页面置换算法移出某些页面,并将这些页面的修改信息写回外存对应的位置中
请求分段:将当前需要的若干个分段装入内存,便可启动作业运行。在作业运行过程中,如果要访问的分段不在内存中,则通过调段功能将其调入,同时还可以通过置换功能将暂时不用的分段换出到外存,以便腾出内存空间。缺点:内存碎片、段置换成本大
工作流程类似
请求段页:当进程运行过程中,访问到不在内存的页时,若该页所在的段在内存,则只产生缺页中断,将所缺的页调入内存;若该页所在的段不在内存,则先产生缺段中断再产生缺页中断,将所缺的页调入内存。若进程需要访问的页已在内存,则对页的管理与段页式存储管理相同
页面置换算法
最佳置换算法、FIFO置换算法、LRU置换算法、LFU置换算法
最佳置换算法(OPT,Optimal) :每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。
先进先出置换算法(FIFO) :每次选择淘汰的页面是最早进入内存的页面实现方法:把调入内存的页面根据调入的先后顺序排成一个队列
最近最久未使用置换算法(LRU,least recently used) :每次淘汰的页面是最近最久未使用的页面实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t值最大的。(该算法的实现需要专门的硬件支持,虽然算法性能好,但是实现困难,开销大)
最不经常使用算法LFU:如果数据最近被访问过,那么将来被访问的几率也更高。所以换出访问次数最少的页面。实现方法:每个页面整一个引用计数,被访问就+1,每次替换引用计数最少的页面
栈为什么比堆快
1、栈是程序运行前就已经静态申请好的空间,所以运行时分配几乎不需要时间。而堆是运行时动态申请的
2、栈在一级缓存,堆在二级缓存。cpu有专门的寄存器(esp,ebp)来操作栈,访问一次就能得到数据,堆要两次,第一次得取得指针,第二次才是真正得数据
3、栈有cpu提供的指令支持
什么是内存池
内存池:是一种内存分配方式,在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。
实现方法:申请一些内存块构成链表,被使用就移除,被释放就重新加入,不够就申请新的内存块
死锁、乐观锁、悲观锁
死锁:比如现在有两个线程都需要对方的已占有的资源,但是双方谁也不让步,导致谁也满足不了,这就是发生了死锁。
乐观锁:乐观锁比较乐观,当多线程并发访问共享资源时,认为发生冲突的概率不大。所以它的工作方式就是,访问资源前不需要上锁,先修改完共享资源,再验证这段时间有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发生有其他线程已经修改过这个资源,放弃本次操作
应用在共享文档,多人同时对文档进行读写,利用版本号来对齐数据。
悲观锁:悲观锁比较悲观,当多线程并发访问共享资源时,认为很可能发生冲突,所以工作方式就是访问资源前必须上锁。互斥锁、读写锁、自旋锁这些都是典型的悲观锁。
如何避免死锁
造成死锁的四个必要条件,缺一不可。
互斥条件:多个线程不能同时使用同一资源
持有并等待条件:线程占有某资源的同时等待其他资源
不可剥夺条件:资源被占有后不可被被其他获取
环路等待条件:多个线程获取资源的顺序构成了死循环
避免死锁的方法:破坏环路等待条件,使用资源有序分配,也就是所有资源都服务于一个线程,不要雨露均沾。