根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
这些系统调用按功能大致可分为如下几类:
系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。与其让操作系统调度,不如我自己来,这就是协程
总结:协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。
为了使参与并发执行的每个程序,包含数据都能独立地运行,在操作系统中必须为之配置一个专门的数据结构,称为进程控制块(PCB,Process Control Block)。进程与PCB是一一对应的,用户进程不能修改。
进程的创建
进程的撤销
线程的创建
线程的撤销
进程全部可分为五种状态分别是:创建状态、就绪状态、运行状态、阻塞状态、终止状态。 在运行期间主要是三种状态:就绪、运行、阻塞状态。
https://www.cnblogs.com/xiaolincoding/p/13631224.html
由于前两点原因,因此需要保持线程间的同步,即一个线程消费(或生产)完,其他线程才能进行竞争CPU,获得消费(或生产)的机会。对于这一点,可以使用条件变量进行线程间的同步:生产者线程在product之前,需要wait直至获取自己所需的信号量之后,才会进行product的操作;同样,对于消费者线程,在consume之前需要wait直到没有线程在访问共享区(缓冲区),再进行consume的操作,之后再解锁并唤醒其他可用阻塞线程。
伪代码实现
假设缓冲区大小为10,生产者、消费者线程若干。生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。
items代表缓冲区已经使用的资源数,spaces代表缓冲区可用资源数
mutex代表互斥锁
buf[10] 代表缓冲区,其内容类型为item
in、out代表第一个资源和最后一个资源
var items = 0, space = 10, mutex = 1;
var in = 0, out = 0;
item buf[10] = { NULL };
producer {
while( true ) {
wait( space ); // 等待缓冲区有空闲位置, 在使用PV操作时,条件变量需要在互斥锁之前
wait( mutex ); // 保证在product时不会有其他线程访问缓冲区
// product
buf.push( item, in ); // 将新资源放到buf[in]位置
in = ( in + 1 ) % 10;
signal( mutex ); // 唤醒的顺序可以不同
signal( items ); // 通知consumer缓冲区有资源可以取走
}
}
consumer {
while( true ) {
wait( items ); // 等待缓冲区有资源可以使用
wait( mutex ); // 保证在consume时不会有其他线程访问缓冲区
// consume
buf.pop( out ); // 将buf[out]位置的的资源取走
out = ( out + 1 ) % 10;
signal( mutex ); // 唤醒的顺序可以不同
signal( space ); // 通知缓冲区有空闲位置
}
}
不能将线程里两个wait的顺序调换否则会出现死锁。例如(调换后),将consumer的两个wait调换,在producer发出signal信号后,如果producer线程此时再次获得运行机会,执行完了wait(space),此时,另一个consumer线程获得运行机会,执行了 wait(mutex) ,如果此时缓冲区为空,那么consumer将会阻塞在wait(items),而producer也会因为无法获得锁的所有权所以阻塞在wait(mutex),这样两个线程都在阻塞,也就造成了死锁。
这四个条件是产生死锁的 必要条件 ,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
通过算法,其中最具有代表性的 避免死锁算法 就是 Dijkstra 的银行家算法,银行家算法用一句话表达就是:当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就真的分配资源给该进程。
解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。
就是线程1持有对象1的锁,线程2持有对象2的锁,然后两者都等待对方释放其持有对象的锁,然后一直等,等到死亡。
public class DieLock {
public static Object t1 = new Object();
public static Object t2 = new Object();
public static void main(String[] args){
new Thread(){
@Override
public void run(){
synchronized (t1){
System.out.println("Thread1 get t1");
try {
Thread.sleep(100);
}catch (Exception e){
}
synchronized (t2){
System.out.println("Thread2 get t2");
}
}
}
}.start();
new Thread(){
@Override
public void run(){
synchronized (t2){
System.out.println("Thread2 get t2");
try {
Thread.sleep(100);
}catch (Exception e){
}
synchronized (t1){
System.out.println("Thread2 get t1");
}
}
}
}.start();
}
}
操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
4.段页式管理机制 :段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
简单来说:页是物理单位,段是逻辑单位。分页可以有效提高内存利用率,分段可以更好满足用户需求。
共同点 :
区别 :
原理必看
分页是一种操作系统中的内存管理技术,将物理内存划分为固定大小的页框,同时将逻辑地址空间分割成相同大小的页。分页管理的核心是通过页表来映射逻辑地址和物理地址。
在分页管理中,由于页表需要频繁地进行访问,因此使用快表可以显著提高分页效率。快表是页表的一种缓存,保存了最近使用的一些页表项,这些页表项可以直接用于地址转换,从而避免了访问主页表的开销。
多级页表是一种页表的组织方式,将一张大的页表分成多个小的页表,每个小的页表称为页目录,其中每个页目录项指向一个页表,每个页表中的页表项则指向实际的物理页框。这样做可以有效地减小页表的大小,从而节约内存空间。此外,多级页表还可以支持虚拟地址空间的非连续分配,允许虚拟地址空间被划分为多个区域,每个区域使用不同的页表。
多级页表和快表的组合是现代操作系统中常用的内存管理技术,可以显著提高分页效率和节约内存空间。在具体实现中,不同的操作系统会有不同的设计方案和参数设置,例如页表的大小、页目录的数量等等
具体原理
操作系统的快表(Translation Lookaside Buffer, TLB)是一种缓存机制,用于加速虚拟内存到物理内存地址的转换过程。
当CPU访问内存时,它会先尝试从TLB中查找相应的内存地址是否已经被缓存。如果TLB中存在该地址的映射,CPU就可以直接使用这个映射而不必访问内存管理单元(MMU)进行地址转换。这种方式比每次都访问MMU进行地址转换要快得多,因为TLB通常被设计成位于CPU内部或非常接近CPU,可以直接从快速缓存中访问。
如果CPU在TLB中找不到所需的内存地址映射,则需要向MMU发出请求,获取该地址的物理内存地址,然后把该映射添加到TLB中。由于TLB的大小有限,当TLB中的所有项都被使用并且没有可用项时,CPU需要把一些现有的TLB项淘汰以腾出空间。
快表通常被用于支持虚拟内存系统,在这种系统中,每个进程都认为它有自己的一块连续内存地址空间,但实际上它们共享物理内存。TLB的作用是把虚拟地址转换为物理地址,从而支持多个进程同时使用物理内存。
多级页表是一种用于管理虚拟内存的数据结构,它把虚拟地址空间划分成多个层级的页表。每个页表包含一组页表项,每个页表项记录了虚拟地址与物理地址之间的映射关系。通过多级页表,操作系统可以支持更大的虚拟地址空间,并且可以更高效地管理和访问页表。
多级页表通常由一个或多个页目录表和一个页表组成。页目录表包含一组页目录项,每个页目录项指向一个页表。页表包含一组页表项,每个页表项记录了一个虚拟页的映射信息。当一个进程访问虚拟地址时,操作系统会先根据虚拟地址的高位找到对应的页目录项,然后根据页目录项中的指针找到对应的页表,最后根据虚拟地址的低位在页表中查找对应的页表项,以获取物理地址。
多级页表的主要优点是它可以有效地降低页表的空间需求。由于虚拟地址空间非常大,单个页表的大小可能会非常大,需要大量的页表项。使用多级页表,操作系统可以把虚拟地址空间划分成多个部分,每个部分由一个小的页表来管理,这样可以大大降低单个页表的大小。同时,多级页表还可以提高访问页表的效率,因为只有在必要时才需要访问每个级别的页表。
虚拟地址和物理地址都是计算机系统中用于访问内存的地址。
物理地址是指计算机内存中的实际物理位置,它是唯一的,直接指向存储器中的物理位置。
虚拟地址是指由CPU生成的地址,它不直接指向实际物理位置,而是由操作系统和硬件协作完成地址映射,将虚拟地址映射到物理地址。虚拟地址空间是操作系统给每个进程分配的独立地址空间,每个进程都可以认为自己独占整个内存空间,而不必担心与其他进程的内存冲突。操作系统通过虚拟地址来管理进程的内存访问,实现内存保护和内存共享等功能。
为什么要有虚拟地址空间呢?主要有以下几个原因:
总之,虚拟地址空间的引入可以使得操作系统更好地管理内存,提高内存利用率和安全性,简化程序设计,同时也方便了内存扩展。
这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。为什么可以这样呢? 正是因为 虚拟内存 的存在,通过 虚拟内存 可以让程序拥有超过系统物理内存大小的可用内存空间。另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
虚拟内存是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,并且把内存扩展到硬盘空间。
虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。 虚拟内存的实现有以下三种方式:
这里多说一下?很多人容易搞混请求分页与分页存储管理,两者有何不同呢?
请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序所需的全部地址空间都装入主存,这也是请求分页存储管理可以提供虚拟内存的原因,我们在上面已经分析过了。
它们之间的根本区别在于是否将一作业的全部地址空间同时装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存。
不管是上面那种实现方式,我们一般都需要:
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。
缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
socket的TCP三次连接
总的来说,由于进程需要分配更多的资源、进行更多的上下文切换、使用更多的进程间通信机制和提供更高的安全性,因此创建或撤销进程的开销比线程要大。
进程切换比线程切换开销大是因为进程切换时要切页表,而且往往伴随着页调度,因为进程的数据段代码段要换出去,以便把将要执行的进程的内容换进来。本来进程的内容就是线程的超集。而且线程只需要保存线程的上下文(相关寄存器状态和栈的信息)就好了,动作很小。
BufferCache和PageCache都是计算机操作系统中用于缓存数据的机制,但是它们针对的数据类型和缓存的位置略有不同。
BufferCache主要是用于缓存文件系统中的数据块,是文件系统缓存的一种实现。当应用程序需要访问文件系统中的某个数据块时,BufferCache会先查看缓存中是否已经有该数据块的副本,如果有,就直接返回该数据块;如果没有,则会从磁盘上读取该数据块,并将其缓存到BufferCache中,以便以后更快地访问。BufferCache是位于文件系统内核中的缓存区域,可以减少磁盘I/O的次数,提高文件系统性能。
PageCache则是用于缓存内存中的页面数据,是虚拟内存系统的一种实现。当进程需要访问某个页面时,PageCache会先查看缓存中是否已经有该页面的副本,如果有,就直接返回该页面;如果没有,则会从磁盘上读取该页面,并将其缓存到PageCache中,以便以后更快地访问。PageCache是位于内存中的缓存区域,可以减少磁盘I/O的次数,提高系统的性能。
因此,BufferCache和PageCache都是用于缓存数据的机制,但是它们的缓存对象和缓存位置略有不同,BufferCache用于缓存文件系统中的数据块,而PageCache用于缓存内存中的页面数据。
孤儿进程是指父进程先于子进程结束,导致子进程成为没有父进程的进程。这种情况通常是由于父进程异常终止或者父进程没有正确等待子进程的结束导致的。孤儿进程会被init进程(PID为1的进程)接管,成为init进程的子进程,从而不会影响系统的正常运行。孤儿进程在被接管前可能会一直占用系统资源,因此及时清理孤儿进程对系统的稳定性和安全性很重要。
僵尸进程是指一个已经完成执行的进程,但其父进程还没有对其进行善后处理,即没有使用wait()或waitpid()等函数来回收子进程的进程描述符和其他资源,导致子进程的进程描述符没有被释放,成为僵尸进程。僵尸进程不占用系统资源,但是会占用进程ID,过多的僵尸进程会耗尽系统的进程ID资源,从而导致系统无法创建新的进程。为避免产生过多的僵尸进程,父进程应该及时回收子进程的资源。
总的来说,孤儿进程和僵尸进程都是进程状态的一种,但是它们的产生原因和表现形式略有不同。孤儿进程是由于父进程先于子进程结束导致的,会被init进程接管;而僵尸进程是由于父进程没有及时回收子进程的资源导致的,会占用进程ID。
CPU负载(CPU load)是指正在运行的进程或任务占用CPU资源的程度,通常使用平均负载(load average)来表示。平均负载是指在特定时间段内,CPU正在运行的进程或任务的平均数量,即可运行进程的平均长度。
在Unix/Linux系统中,平均负载是通过对系统的运行状态进行采样并计算得出的。一般情况下,平均负载的值是一个实数,表示正在运行的进程和等待CPU资源的进程数之和。例如,一个平均负载为1的系统表示正在运行的进程数等于CPU核心数,而一个平均负载为2的系统表示有两倍于CPU核心数的进程在等待CPU资源。
CPU负载是衡量系统负载的一个重要指标,高负载表示系统运行的任务较多或者CPU资源不足,可能会导致系统的响应变慢或者出现卡顿现象。因此,管理员可以根据CPU负载情况进行资源调度或者优化系统配置,以保证系统的稳定性和高效性。