字节面试题——操作系统,附答案

1.读写锁,不同点,应用场景

区分读和写,处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。
注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写优先于读,当有线程因为等待写锁而进入睡眠时,则后续读者也必须等待

应用场景
适用于读取数据的频率远远大于写数据的频率的场合。

扩展

加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。

互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒

自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源

区别:

当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。

互斥锁加锁失败后,线程会释放 CPU ,给其他线程;那这个开销成本是什么呢?会有两次线程上下文切换的成本:当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
自旋锁加锁失败后,线程会忙等待,直到它拿到锁;自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,

RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高

信号量:semaphore,是用于线程间同步的,当一个线程完成操作后就通过信号量通知其它线程,然后别的线程就可以继续进行某些操作了。

信号量和互斥锁的区别:semaphore

信号量是用于线程间同步的,而互斥锁是用于线程的互斥的
互斥量的获取和释放都是在同一线程中完成的,pthread_mutex_lock(),pthread_mutex_unlock()。而信号量的获得和释放是在不同的线程的操作为sem_wait(),sempost();
互斥量的值只能为0和1,而信号量只要value>0,其它线程就可以sem_wait成功,成功后信号量value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value加1。因此信号量的值可以为非负整数

2.什么是死锁,死锁的条件

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的原因:

系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
进程推进顺序不合理 :进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

死锁的四个条件是:

  1. 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占用。此时若有其他进程请求该资源,则请求进程只能等待。
  2. 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。

  3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  4. 循环等待条件:存在一种进程资源的循环等待链,连中每一个进程已获得的资源同时被链中下一个进程所请求。

死锁只有在这四个条件同时满足时出现。

2.处理死锁的四种方法:


(1)死锁预防:如果想在程序运行之前预防发生死锁(也成为 “死锁预防”),必须设法破坏产生死锁的四个必要条件之一

破坏互斥条件:允许系统资源都能共享使用,则系统不会进行死锁状态。这种方案并不太可行,因为有些资源根本就不能同时访问,比如打印机。

破坏不剥夺条件:当一个已经保持了某些不可剥夺资源的进程,请求新的资源时得不到满足,它必须释放已经保持的所有资源,待以后需要时再重新申请。这种方法常用于状态易于保存和恢复的资源,如 CPU 的寄存器及内存资源,一般不能用于打印机之类的资源。

破坏请求和保持条件:采用预先静态分配方法,即进程在运行前一次申请完他所需要的全部资源,在他的资源未满足前,不把它投入运行。一旦运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。

破坏循环等待条件:采用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源,则该进程在以后的资源申请中,只能申请编号比之前大的资源。

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)。只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请求保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

以下有几种方案:

1.以确定的顺序获得锁

2、超时放弃:当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

3、使用ThreadLocal,各个线程本身都有各自的访问资源,以防止对互斥资源访问引起的死锁

4、使用CAS,轻量级循环,而不上升到重量级的锁

(2)死锁检测:允许死锁的发生,但是可以通过系统设置的检测结构及时的检测出死锁的发生,采取一些措施,将死锁清除掉

(3)死锁避免:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁

最具有代表性的避免死锁算法是银行家算法。
银行家算法: 操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。

(4)死锁解除:与死锁检测相配套的一种措施。当检测到系统中已发生死锁,需将进程从死锁状态中解脱出来。

剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

3.预防死锁的方法

  1. 破坏“互斥”条件:
    由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
  2. 破坏“持有且等待”条件:
    方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
    方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
  3. 破坏“禁止抢占”条件:
    当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
  4. 破坏“循环等待”条件:
    可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。

死锁预防 :核心思想是,死锁只有在四个条件同时满足时出现。预防死锁就是至少破坏这四个条件其中一项,即破坏“禁止抢占”、破坏“持有等待”、破坏“资源互斥”和破坏“循环等待”。

4.两种死锁避免算法:


*进程启动拒绝:如果一个进程的请求会导致死锁,则不启动该进程。
*资源分配拒绝:如果一个进程增加的资源请求会导致死锁,则不允许此分配

死锁避免和死锁预防的区别:

  死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现;而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。

检测死锁的算法:


1.在资源分配图中,找出既不阻塞又不是孤点的进程。消去它所有的请求边和分配边,使之称为孤立的结点
2.进程所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。若能消去途中所有的边,则称该图是可完全简化的。

死锁的解除:


*两种常用的死锁解除方法:

1.资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
2.撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能己经运行了很长时间,己经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
3.进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

如何决定“对谁动手”
1.进程优先级
2.己执行多长时间
3.还要多久能完成
4.进程己经使用了多少资源
5.进程是交互式的还是批处理式的


3.说说什么是进程?什么是线程?区别?

进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位一般由程序、数据集合和进程控制块三部分组成

线程(thread)是操作系统能够进行运算调度的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行

一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存,线程之间可以共享对象、资源,如果有冲突或需要协同,还可以随时沟通以解决冲突或保持同步

区别

本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

线程间通信的常用方式

多个线程在并发执行的时候,他们在CPU中是随机切换执行的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务,线程通信一般有4种方式:

  • 通过 volatile 关键字
  • 通过 Object类的 wait/notify 方法
  • 通过 condition 的 await/signal 方法
  • 通过 join 的方式

 1. volatile 是共享内存的,两个线程共享一个标志位,当标志位更改的时候就执行不同的线程。但是对于volatile关键字这个方法来说,我们的线程执行了很多次空循环,来等待另外一个线程来获取锁,这种操作无疑是十分消耗CPU的资源的,所以说为了解决这种情况,我们就需要一种机制可以实现线程之间的通信,可以唤醒其他的线程,而不是等待直到自己获取CPU的时间片。

2.Object类提供了三个线程间通信的方法,wait(),notify(),notifyAll()。这三个方法必须都在同步代码块中执行的。

方法名 具体操作
wait()wait()方法执行前,是必须要获得对应的锁的,当执行wait()方法后,线程就会释放掉自己所占有的锁,释放CPU,然后进入阻塞状态,直到被notify()方法唤醒。
notify() 会唤醒一个处于等待该对象锁的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
notifyAll() 和notify()方法差不多,只不过他是唤醒所有等待该对象锁的线程,让他们进入就绪队列,但是谁执行就看谁抢占到CPU,notify()方法也是这样,只不过是唤醒随机的一个而已

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度;反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

3.Condiction对象是通过lock对象来创建得(调用lock对象的newCondition()方法),他在使用前也是需要获取锁得,其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。Condiction对象得常用方法:

方法名 具体操作
await()线程自主释放锁,进入沉睡状态,直到被再次唤醒。
await(long time, TimeUnit unit)线程自主释放锁,进入沉睡状态,被唤醒或者未到达等待时间时一直处于等待状态。
signal()唤醒一个等待线程。
signal()All()唤醒所有等待线程,能够从等待方法返回的线程必须获得与Condition相关的锁。
4.本质就是调用了wait方法,让当前线程阻塞,直到另一个线程执行完毕。(当前线程wait后,执行join方法的线程大概率抢到锁资源,而且当一个线程执行完毕后,会默认调用notifyAll方法。)

准确来说,应该只有三种可以通信的方式,join只是让当前线程先执行完,并没有说根据两个线程之间数据的共享。

进程间的通信方式(六种)

1.管道本质上就是内核中的一个缓存,当进程创建一个管道后,Linux会返回两个文件描述符,一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。

如果想要实现两个进程通过管道来通信,则需要让创建管道的进程fork子进程,这样子进程们就拥有了父进程的文件描述符,这样子进程之间也就有了对同一管道的操作。

缺点:

  1. 半双工通信,一条管道只能一个进程写,一个进程读。
  2. 一个进程写完后,另一个进程才能读,反之同理。
     

2.消息队列:

A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。数据会被分为一个一个的数据单元,称为消息体,消息发送方和接收方约定好消息体的数据类型,不像管道是无格式的字节流类型,这样的好处是可以边发送边接收,而不需要等待完整的数据。

缺点:

  1. 每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的
  2. 消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。

3.共享内存:

共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。

现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。

信号量:

当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。

因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步。

信号量代表着资源的数量,操作信号量的方式有两种:

  • P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
  • V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。

    信号量实现互斥:

    信号量初始化为1

    进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
    若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
    直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。
    (2)信号量实现同步:

    由于多线程下各线程的执行顺序是无法预料的,有些时候我们希望多个线程之间能够密切合作,这时候就需要考虑线程的同步问题。

    信号量初始化为0

    如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
    接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
    最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。

    5.信号:

    在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l命令查看。

    如果是运行在shell终端的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C产生SIGINT信号,表示终止进程。

    如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID产生SIGKILL信号,表示立即结束进程。

    6.Socket:

    前面提到的管道,消息队列,共享内存,信号量和信号都是在同一台主机上进行进程间通信,如果想要跨网络和不同主机上的进程进行通信,则需要用到socket。

    实际上,Socket不仅可以跨网络和不同主机进行进程间通信,还可以在同一主机进行进程间通信。
     

系统如何实现进程调度

1. 选择调度算法:操作系统需要选择适合当前环境的调度算法。常见的调度算法包括先来先服务(FCFS)、最短作业优先(SJF)、优先级调度、轮转调度(RR)等。

2. 创建进程队列:操作系统会维护一个进程队列,用于存储所有需要执行的进程。进程队列可以采用不同的数据结构,如队列、链表等。

3. 进程状态管理:操作系统会为每个进程维护其状态信息,如就绪状态、运行状态、阻塞状态等。进程状态的转换由操作系统根据进程的执行情况进行管理。

4. 进程调度:根据选择的调度算法,操作系统会根据一定的策略从进程队列中选择一个进程,将其从就绪状态转换为运行状态,并分配CPU资源给该进程。

5. 时间片管理:如果采用轮转调度算法,操作系统会设置一个固定的时间片大小,每个进程在运行一段时间后会被暂停,然后切换到下一个进程。这样可以保证每个进程都能获得一定的CPU时间。 6. 中断处理:在进程执行过程中,可能会发生中断事件,如IO请求、时钟中断等。操作系统会根据中断类型进行相应的处理,可能会暂停当前进程的执行,并将其状态转换为阻塞状态,然后选择下一个进程继续执行。

7. 调度策略优化:操作系统可以根据实际情况对调度算法进行优化,以提高系统的性能和响应速度。例如,可以根据进程的优先级、执行时间等因素进行动态调整。

4.堆和栈的区别以及存储模式有什么区别

内存分区模型

栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。它与数据机构中的堆是两回事,分配方式类似于链表。
全局区(静态区)(static):全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另外一块区域。程序结束后由系统释放。

bss段:未初始化或初始化未0的全局变量和静态变量,具体体现为 一个占位符,不占用.exe文件空间,其内容由操作系统初始化/清零

.data段:存放已初始化的全局变量和静态变量和字符串常量,占用 exe,内容由程序初始化,程序结束后资源由系统自动释放 
文字常量区:常量字符串就是放在这里。程序结束后由系统释放。
程序代码区:存放函数体的二进制代码

堆和栈的区别

1申请方式
  • 栈:由系统自动分配。例如在声明函数的一个局部变量int b,系统自动在栈中为b开辟空间。
  • 堆:需要程序员自己申请,并指明大小,在C中用malloc函数;在C++中用new运算符。
2申请后系统的响应
  • 栈:只要栈的剩余空间大于所申请的空间系统将为程序提供内存,否则将报异常提示栈溢出。
  • 堆:操作系统有一个记录空间内存地址的链表,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆节点,然后将节点从内存空闲节点链表中删除,并将该节点的空间分配给程序。对于大多数操作系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的对节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入到链表中。
3申请大小的限制
  • 栈:在Windows下,栈是向低地址拓展的数据结构,是一块连续的内存的区域。站定地址和栈的大小是系统预先规定好的,如果申请的内存空间超过栈的剩余空间,将提示栈溢出
  • 堆:堆是向高地址拓展的内存结构,是不连续的内存区域。是系统用链表存储空闲内存地址的,不连续
4申请效率的比较
  • 栈:由系统自动分配,速度较快。但程序员无法控制。
  • 堆:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。 拓展:在Windows操作系统中,最好的方式使用VirtualAlloc分配内存。不是在堆,不是在栈,而是在内存空间中保留一块内存,虽然用起来不方便,但是速度快,也很灵活。
5堆和栈的存储内容
  • 栈:在函数调用时,第一个进栈的是主函数的中的下一条指令(函数调用的下一个可执行语句)的地址,然后是函数的各个参数。在C编译器中,参数是由右往左入栈的,然后是函数的局部变量。静态变量不入栈。
  • 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。 数据结构方面的堆和栈与上边叙述不同。这里的堆是指优先队列的一种数据结构,第一个元素有最高的优先权;栈实际就是满足先进后出的性质的数学或数据结构。

5.内存泄漏和内存溢出

内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

区别:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用, 系统已经不能再分配出你所需要的空间;内存泄露是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但是内存泄漏次数多了就会导致内存溢出。

6.多线程和多进程,以及应用场景

进程是资源分配的基本单位;线程是系统调度和分派的基本单位。
属于同一进程的线程,堆是共享的,栈是私有的。
属于同一进程的所有线程都具有相同的地址空间。

多进程的优点:
①编程相对容易;通常不需要考虑锁(占用相同资源才会考虑)和同步资源的问题。
②更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
③有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。

多线程的优点:
①创建速度快,方便高效的数据共享
共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术。
②较轻的上下文切换开销 - 不用切换地址空间,不用更改寄存器,不用刷新TLB。
③提供非均质的服务。如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。

多线程相比于多进程占用内存少、CPU利用率高,创建销毁,切换都比较简单,速度很快。多进程相比于多线程共享数据复杂,需要进行进程间通信,但是同步简单,多线程因为数据共享简单,导致同步复杂。多进程编程调试都比多线程简单。进程之间互相不影响,一个线程挂掉将导致整个进程挂掉。多进程适合多核,多机分布,多线程适合多核分布。
 

多线程模型适用于I/O密集型的工作场景,因此I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。同时,多线程模型也适用于单机多核分布式场景。

多进程模型,适用于CPU密集型。同时,多进程模型也适用于多机分布式场景中,易于多机扩展。

①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
②需要进行大量计算的优先使用线程(CPU频繁切换)
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。

③强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

④可能要扩展到多机分布的用进程,多核分布的用线程


7.进程通信方式

进程间通信的方式有:1、无名管道( pipe );2、高级管道(popen);3、有名管道 (named pipe);4、消息队列( message queue );5、信号量( semophore );7、共享内存( shared memory );8、套接字( socket )。

1、无名管道( pipe )

管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2、高级管道(popen)

将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

3、有名管道 (named pipe)

 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

4、消息队列( message queue )

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5、信号量( semophore )

 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

6、信号 ( sinal )

 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

7、共享内存( shared memory )

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是非常快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

8、套接字( socket )

套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

8.多线程如何实现的

多线程并发:多线程是实现并发(双核的真正并行或者单核机器的任务切换都叫并发)的一种手段,多线程并发即多个线程同时执行,一般而言,多线程并发就是把一个任务拆分为多个子任务,然后交由不同线程处理不同子任务,使得这多个子任务同时执行。

C++多线程并发: (简单情况下)实现C++多线程并发程序的思路如下:将任务的不同功能交由多个函数分别实现,创建多个线程,每个线程执行一个函数,一个任务就这样同时分由不同线程执行了。

9.计算机内存管理的方式

3.1、内存管理基础 | 王道考研操作系统知识点整理

简单的分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,比如块式管理。非连续分配管理方式允许一个程序内存分散,比如页式管理、段式管理和段页式管理。

1. 块式管理:远古时代的计算机操作系统的内存管理方式,将内存分为几个固定大小的块,每个块只包含一个进程,如果程序运行需要内存,操作系统就给它分配一块,如果程序运行只需要很小的空间,则分配的这块内存很大一部分就浪费了,这些在每个块中未被利用的空间,我们称为碎片。

2. 页式管理:把主存分为大小相等且固定的一页一页的形式,页比较小,相对于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。

3. 段式管理:页式管理虽然提高了内存利用率,但是其中的页没有任何实际意义,段式管理把主存分为一段一段 ,每一段的空间又比一页的空间小很多。但是段有实际意义,段式管理通过段表对应逻辑地址和物理地址。

4. 段页式管理:段页式管理机制结合了段式管理和页式管理的优点,就是把主存分为若干段,每个段又分为若干页,也就是说段页式管理机制中段和段之间以及段的内部都是离散的。


9.进程的状态,进程状态就绪和等待状态的区别是什么
 

等待态(阻塞):等待某个事件的完成;

2.就绪态:等待系统分配处理器以便运行;

3.运行态:占有处理器正在运行。

运行态→等待态 往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。

等待态→就绪态 则是等待的条件已满足,只需分配到处理器后就能运行。

运行态→就绪态 不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。

就绪态→运行态 系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态

10.进程调度方法

调度算法是指:根据系统的资源分配策略所规定的资源分配算法

1 先来先服务调度算法(队列)(FCFS)First Come First Served

2 短作业(进程)优先调度算法(优先队列)(SJF)Shortest Job First 

3 优先权调度算法 (PSA)priority-scheduling algorithm

4.高响应比优先调度算法 (HRRN) HighestResponseRatioNext

使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

5 基于时间片的轮转调度算法(RoundRobin)

6 多级反馈队列调度算法(Feedback)

10.进程和线程的上下文切换

上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态。

线程上下文切换和进程上下文切换的区别:

 1 线程切换发生在CPU保存线程当前状态并切换到同一进程的另一个线程时。 进程切换发生在操作系统的调度程序保存正在运行的 Program 的当前状态(包括 PCB 的状态)并切换到另一个程序时。

2 线程切换帮助CPU同时处理多个线程。 进程切换 涉及加载新程序的执行状态。

3 线程切换不涉及内存地址空间的切换,所有处理器帐户保持保存的内存地址。 PCS 涉及内存地址空间的切换。处理器帐户的所有内存地址都会被刷新。

4 线程切换处理器的缓存和平移后备缓冲区保留它们的状态。 进程切换处理器的缓存和 TLB 被刷新。

5 线程切换虽然涉及寄存器和堆栈指针的切换,但它不承担改变地址空间的成本,因此效率更高。 进程切换涉及更改地址空间的沉重成本。因此效率较低。

6 线程切换 更快更便宜。 进程切换 相对较慢且成本较高。

10协程

协程(Coroutine)是一种轻量级的用户级线程,实现的是非抢占式的调度,即由当前协程切换到其他协程由当前协程来控制。是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处继续运行。 一个线程内的多个协程的运行是串行的,这点和多进程(多线程)在多核CPU上执行时是不同的。 多进程(多线程)在多核CPU上是可以并行的。当线程内的某一个协程运行时,其它协程必须挂起。

协程切换

由于协程切换是在线程内完成的,涉及到的资源比较少。不像内核级线程(进程)切换那样,上下文的内容比较多,切换代价较大。协程本身是非常轻巧的,可以简单理解为只是切换了寄存器和协程栈的内容。这样代价就非常小。

优点

  • 协程更加轻量,创建成本更小,降低了内存消耗
  • 协程有自己的调度器,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率
  • 减少同步加锁,整体上提高了性能
  • 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调

缺点

  • 在协程执行中不能有阻塞操作,否则整个线程被阻塞
  • 协程可以处理 IO 密集型程序的效率问题,但不适合处理 CPU 密集型问题

11线程池

线程池其实是一种池化的技术的实现,池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

 12什么是Linux用户态和内核态

一、内核态、用户态概念
内核态:也叫内核空间,是内核进程/线程所在的区域。主要负责运行系统、硬件交互。

用户态:也叫用户空间,是用户进程/线程所在的区域。主要用于执行用户程序。

二、内核态和用户态的区别
内核态:运行的代码不受任何限制,CPU可以执行任何指令。

用户态:运行的代码需要受到CPU的很多检查,不能直接访问内核数据和程序,也就是说不可以像内核态线程一样访问任何有效地址。

操作系统在执行用户程序时,主要工作在用户态,只有在其执行没有权限完成的任务时才会切换到内核态。

三、为什么要区分内核态和用户态
保护机制。防止用户进程误操作或者是恶意破坏系统。内核态类似于C++的私有成员,只能在类内访问,用户态类似于公有成员,可以随意访问。

线程安全的实现方式

C++的线程安全的方法有以下几种:

  1. 互斥量(Mutex):通过互斥量锁定代码块,以保证只有一个线程同时访问该代码。
  2. 条件变量(Condition variable):在互斥量的基础上,当等待执行的线程满足条件时,唤醒执行。
  3. 原子操作(Atomic operation):通过C++11提供的头文件内置的原子操作类型,直接对数据进行操作,避免加锁带来的性能损失。
  4. 信号量(Semaphore):通过信号量管理线程的并发访问,保证合理的资源分配。
  5. 读写锁(Read-write lock):读写锁分为读锁和写锁,读锁允许多个线程同时读,写锁只允许一个线程写。 这些方法可以根据具体的需求选择使用。

13.虚拟内存是干嘛的

虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片。还有部分暂时存储在外部磁盘存储器上(Swap),在需要时进行数据交换。

它允许操作系统将物理内存和磁盘存储器组合起来使用,使得应用程序能够使用比实际可用内存更多的内存空间。虚拟内存的核心思想是将内存分成大小相同的页面(page),并将其映射到磁盘上的一个交换文件(swap file)中。

当应用程序访问一个尚未分配的页面时,操作系统会将其加载到物理内存中。当物理内存不足时,操作系统会将一部分页面置换到交换文件中,以便腾出物理内存供新的页面使用。这个过程称为页面置换(page swapping)。

常用的页面置换算法(如最近最少使用算法、先进先出算法、时钟算法等)、虚拟内存的优化方法(如预读取、页面合并等)

优点:可以弥补物理内存大小的不足;一定程度的提高反映速度;减少对物理内存的读取从而保护内存延长内存使用寿命;

缺点:占用一定的物理硬盘空间;加大了对硬盘的读写;设置不得当会影响整机稳定性与速度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值