自己在秋招过程中遇到的高频操作系统相关的面试题
内存管理
虚拟内存
- 虚拟内存的⽬的是为了让物理内存扩充成更⼤的逻辑内存,从⽽让程序获得更多的可⽤内存。 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有⾃⼰的地址空间,这个地址空间被分割成多个块,每⼀块称为⼀⻚ 。
- 这些⻚被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有⻚都必须在物理内存中。当程序引⽤到不在物理内存中的⻚时,由硬件执⾏必要的映射,将缺失的部分装⼊物理内存并新执⾏失败的指令。
- 虚拟内存允许程序不⽤将地址空间中的每⼀⻚都映射到物理内存,也就是说⼀个程序不 需要全部调⼊内存就可以运⾏,这使得有限的内存运⾏⼤程序成为可能 。
虚拟内存的容量受操作系统位数限制吗? 不受
- 虚拟内存的容量受操作系统位数限制,但是受限制的因素不仅仅是操作系统的位数。
- 操作系统的位数决定了它能够寻址的物理内存的最大容量。例如,32位操作系统可以寻址最多4GB(2^32字节)的物理内存,而64位操作系统可以寻址的物理内存容量更大。
- 虚拟内存是一种将磁盘空间用作扩展内存的技术,它将物理内存和磁盘空间结合起来,为进程提供了比物理内存更大的地址空间。虚拟内存的容量通常可以远远超过物理内存的容量。
- 虚拟内存的容量受到操作系统和硬件的限制。在操作系统层面,它需要提供适当的机制来管理虚拟内存,包括将虚拟地址映射到物理地址、页面置换算法等。硬件方面,CPU和内存管理单元(MMU)需要支持虚拟内存的相关功能。
- 因此,虚拟内存的容量不仅仅受到操作系统位数的限制,还受到操作系统和硬件的支持程度、算法和配置的影响。在实际应用中,虚拟内存的容量可以根据操作系统和硬件的限制进行配置和调整。
虚拟内存和物理内存是一一对应的吗 ? 不是
- 虚拟内存是计算机系统中的一种技术,它通过将部分程序或数据存储在磁盘上,以扩展可用的地址空间。每个进程都拥有自己的虚拟地址空间,这使得每个进程都认为它拥有连续且私有的内存空间。
- 物理内存是实际的硬件内存,它是计算机系统中用于存储程序和数据的物理存储区域。它由RAM(Random Access Memory)芯片组成,提供了快速的访问速度。
- 在虚拟内存系统中,虚拟内存和物理内存之间存在映射关系。当程序访问虚拟内存时,操作系统将虚拟内存地址转换为对应的物理内存地址。这个转换过程是由操作系统的内存管理单元(MMU)和页表来完成的。
- 虚拟内存的大小可以远远超过物理内存的大小。当程序访问的数据不在物理内存中时,会发生缺页中断,操作系统将相应的页面从磁盘加载到物理内存中,再将访问重定向到正确的物理内存地址上。
- 虚拟内存和物理内存之间并不是一一对应的关系,而是通过页表和映射机制实现地址转换和管理。这使得系统可以管理更大的地址空间,提供更灵活的内存管理和更高的程序执行效率。
不同的进程可以通过虚拟内存来共享物理内存吗 ? 可以
- 虚拟内存提供了一种机制,使得多个进程可以将相同的物理内存页面映射到各自的虚拟地址空间中。这种共享内存的方式称为共享页(Shared Pages)或共享内存段(Shared Memory Segment)。
- 在共享内存的情况下,多个进程可以访问和修改相同的物理内存区域,从而实现进程间的数据共享。这种共享方式在需要高效地进行进程间通信和数据交换的场景下非常有用。
- 共享内存的实现通常涉及以下步骤:
- 创建共享内存区域:操作系统提供了特定的系统调用或库函数来创建共享内存区域,将其映射到进程的虚拟地址空间。
- 映射共享内存:各个进程使用相同的键或名称打开共享内存区域,并将其映射到各自的虚拟地址空间中。
- 访问和同步:进程可以通过读取和写入共享内存来进行数据共享。为了确保多个进程之间的同步和一致性,通常需要使用同步机制(例如信号量、互斥锁等)来控制对共享内存的访问。
共享内存是一种底层的通信机制,需要由进程自己来协调和管理数据的一致性和同步。因此,在使用共享内存时,必须小心处理并发访问和数据一致性的问题,以避免竞态条件和数据损坏等风险。
进程调度算法
先来先服务 first-come first-serverd(FCFS)
⾮抢占式的调度算法,按照请求的顺序进⾏调度。 有利于⻓作业,但不利于短作业,因为短作业必须⼀直等待前⾯的⻓作业执⾏完毕才能执⾏,⽽⻓作业⼜需要执⾏ 很⻓时间,造成了短作业等待时间过⻓。
短作业优先 shortest job first(SJF)
⾮抢占式的调度算法,按估计运⾏时间最短的顺序进⾏调度。 ⻓作业有可能会饿死,处于⼀直等待短作业执⾏完毕的状态。因为如果⼀直有短作业到来,那么⻓作业永远得不到 调度。
最短剩余时间优先 shortest remaining time next(SRTN)
最短作业优先的抢占式版本,按剩余运⾏时间的顺序进⾏调度。 当⼀个新的作业到达时,其整个运⾏时间与当前进 程的剩余时间作比较。 如果新的进程需要的时间更少,则挂起当前进程,运⾏新的进程。否则新的进程等待。
时间⽚轮转
-
将所有就绪进程按 FCFS 的原则排成⼀个队列,每次调度时,把 CPU 时间分配给队⾸进程,该进程可以执⾏⼀个时 间⽚。
-
当时间⽚⽤完时,由计时器发出时钟中断,调度程序便停⽌该进程的执⾏,并将它送往就绪队列的末尾,同时继续 把 CPU 时间分配给队⾸的进程。
-
时间⽚轮转算法的效率和时间⽚的⼤⼩有很⼤关系:
因为进程切换都要保存进程的信息并且载⼊新进程的信息,如果时间⽚太⼩,会导致进程切换得太频繁,在进 程切换上就会花过多时间。 ⽽如果时间⽚过⻓,那么实时性就不能得到保证。
优先级调度
为每个进程分配⼀个优先级,按优先级进⾏调度。 为了防⽌低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
多级反馈队列
- ⼀个进程需要执⾏ 100 个时间⽚,如果采⽤时间⽚轮转调度算法,那么需要交换 100 次。
- 多级队列是为这种需要连续执⾏多个时间⽚的进程考虑,它设置了多个队列,每个队列时间⽚⼤⼩都不同,例如 1,2,4,8,…。进程在第⼀个队列没执⾏完,就会被移到下⼀个队列。
- 这种⽅式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上⾯的优先权最⾼。因此只有上⼀个队列没 有进程在排队,才能调度当前队列上的进程。
- 可以将这种调度算法看成是时间⽚轮转调度算法和优先级调度算法的结合
死锁
死锁怎么产生的
死锁会产生的话一般就是资源互相占用,但是没有办法解锁,形成循环这样的情况,比如说 a 线程有一部分 b 线程需要的资源, b 线程有一部分 a 需要的资源,那他两个人互相的互斥等待形成了死锁,两个线程都没有办法完成任务。
补充:
死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。
所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。
怎么避免死锁
- 避免死锁的话可以手动的 kill 掉某一个进程来结束当前的死锁状态。
- 也可以说设置一些抢占的规则。如果这个进程占用的时间非常长的话,通过上下文切换给另外一个进程运行的机会,
- 也可以在分配资源的时候进行预先的设计,就是有一个银行家算法来进行一个不会发生死锁的进程运行的调度
多线程锁是什么
多线程锁是一种用来保护共享资源的机制。在多线程编程中,如果多个线程同时访问同一个共享资源,可能会发生竞态条件(Race Condition),导致程序的行为出现未定义的情况。为了避免这种情况的发生,可以使用多线程锁来保护共享资源。
多线程锁的基本思想是,在访问共享资源之前先获取锁,访问完成之后再释放锁。这样可以保证同一时刻只有一个线程可以访问共享资源,从而避免竞态条件的发生。
常见的多线程锁包括互斥锁、读写锁、条件变量等。其中,互斥锁用于保护共享资源的访问,读写锁用于在读多写少的情况下提高并发性能,条件变量用于线程之间的同步和通信。
线程和进程有什么区别
进程是程序在操作系统中的一次执行过程,它拥有独立的地址空间和系统资源。线程是进程中的一个执行单元,同一进程内的多个线程共享相同的地址空间和系统资源。
补充:
- 进程是资源调度的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序;线程是程序执行的基本单位,每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
- 每个进程有自己的独立地址空间,不与其他进程分享;一个进程里可以有多个线程,彼此共享同一个地址空间。堆内存、文件、套接字等资源都归进程管理,同一个进程里的多个线程可以共享使用。每个进程占用的内存和其他资源,会在进程退出或被杀死时返回给操作系统。
- 并发应用开发可以用多进程或多线程的方式。多线程由于可以共享资源,效率较高;反之,多进程(默认)不共享地址空间和资源,开发较为麻烦,在需要共享数据时效率也较低。但多进程安全性较好,在某一个进程出问题时,其他进程一般不受影响;而在多线程的情况下,一个线程执行了非法操作会导致整个进程退出。
进程通信:不包含回调函数和全局变量
管道
有名管道
- 有名管道是FIFO⽂件,存在于⽂件系统中,可以通过⽂件路径名来指出。
- 有名管道可以在不具有亲缘关系的进程间进⾏通信
无名管道
- ⽆名管道是⼀种特殊的⽂件,这种⽂件只存在于内存中。
- ⽆名管道只能⽤于⽗⼦进程或兄弟进程之间,必须⽤于具有亲缘关系的进程间的通信。
- ⽆名管道只能由⼀端向另⼀端发送数据,是半双⼯⽅式,如果双⽅需要同时收发数据需要两个管 道
共享内存
- 进程可以将同⼀段共享内存连接到它们⾃⼰的地址空间,所有进程都可以访问共享内存中的地址,如果某个进程向 共享内存内写⼊数据,所做的改动将⽴即影响到可以访问该共享内存的其他所有进程
- 共享内存的⽅式像极了多线程中线程对全局变量的访问,⼤家都对等地有权去修改这块内存的值,这就导致在多进程并发下,最终结果是不可预期的。所以对这块临界区的访问需要通过信号量来进⾏进程同步。
- 可以通过共享内存进⾏通信的进程不需要像⽆名管道⼀样需要通信的进程间有亲缘关系。其次内存共享的速度也⽐管道快,不存在读取⽂件、消息传递等过程,只需要到相应映射到的内存 地址直接读写数据即可。
消息队列
- 消息队列可以独⽴于读写进程存在,从⽽避免了 FIFO 中同步管道的打开和关闭时可能产⽣的困难
- 避免了 FIFO 的同步阻塞问题,不需要进程⾃⼰提供同步⽅法
- 读进程可以根据消息类型有选择地接收消息,⽽不像 FIFO 那样只能默认地接收
信号量
- 提到共享内存⽅式时也提到,进程共享内存和多线程共享全局变量⾮常相似。所以在使⽤内存共享的⽅式是也需要通过信号量来完成进程间同步。多线程同步的信号是POSIX信号量,⽽在进程⾥使⽤SYSTEM V信号量
- 信号量是一种用于进程间同步和互斥的机制。它可以用来保护共享资源,防止多个进程同时访问
套接字
套接字是一种网络编程中常用的通信方式,可以在不同的主机之间进行进程间通信。套接字提供了一种统一的接口,使得进程可以通过网络进行通信
RPC(Remote Procedure Call):
远程过程调用是一种进程间通信的方式,允许一个进程调用另一个进程中的过程或函数,像调用本地过程一样
在写多线程代码时,有什么方式可以保证同步
加在多线程编程中,确保线程之间的同步是非常重要的,以避免竞态条件和数据不一致等问题。以下是几种常见的同步方式:
- 互斥锁(Mutex):
- 使用互斥锁可以保护临界区(Critical Section),只允许一个线程在同一时间内访问共享资源。
- 当一个线程获得互斥锁时,其他线程需要等待该锁释放才能进入临界区。
- 互斥锁可以通过标准库中的
std::mutex
实现,使用std::lock_guard
或std::unique_lock
进行自动锁定和解锁。
- 条件变量(Condition Variable):
- 条件变量用于线程之间的通信和同步,允许线程等待特定的条件发生。
- 当某个线程在等待条件变量时,它会被阻塞,直到另一个线程满足了条件并通知条件变量。
- 条件变量可以通过标准库中的
std::condition_variable
实现,使用std::unique_lock
进行等待和通知。
- 原子操作(Atomic Operations):
- 原子操作是一种无需使用锁即可保证数据的原子性的操作。
- 原子操作可以确保在多线程环境中对共享数据的读取和写入操作是原子的,不会发生数据竞争。
- 原子操作可以通过标准库中的
std::atomic
模板进行实现,如std::atomic<int>
。
- 信号量(Semaphore):
- 信号量是一种同步原语,用于控制多个线程对共享资源的访问。
- 信号量可以用来限制同时访问共享资源的线程数量,以及进行线程之间的等待和通知。
- 信号量可以通过操作系统提供的原语或第三方库进行实现,如
std::counting_semaphore
(C++20标准)。
5. 原始线程同步方式: - 除了上述高级同步方式,还可以使用原始的线程同步方式,如条件变量、互斥锁、原子操作等的组合,手动实现同步和互斥。
- 这种方式需要更多的注意事项和更细致的代码控制,但在某些特定的场景下可能更加灵活和高效。
6. 原子操作和无锁数据结构: - 使用原子操作和无锁数据结构可以避免互斥锁的使用,提高并发性能。
- 原子操作可以用于对共享数据进行原子读取和写入操作,而无锁数据结构(如无锁队列、无锁哈希表)可以避免锁竞争。
7. 线程局部存储(Thread-Local Storage): - 线程局部存储允许每个线程拥有自己的独立变量副本,避免了对共享变量的访问和同步。
- 使用线程局部存储可以提高并发性能,减少对全局共享状态的依赖。
8. 并发数据结构: - 标准库和第三方库提供了一些并发数据结构,如并发队列、并发哈希表等。
- 这些数据结构已经内置了同步机制,可以在多线程环境下安全地进行读写操作。
9. 同步的范围控制: - 尽量将同步的范围控制在最小范围内,避免不必要的同步开销。
- 只在必要时才使用互斥锁、条件变量等同步机制,尽量减小临界区的大小。
10. 使用线程池: - 线程池是一种线程管理机制,可以重用线程来执行多个任务。
- 使用线程池可以减少线程的创建和销毁开销,提高线程利用率和整体性能。
11. 注意死锁和饥饿: - 避免出现死锁(Deadlock)和饥饿(Starvation)的情况。
- 死锁指多个线程相互等待对方释放资源导致的无法继续执行的情况,而饥饿指某个线程由于资源分配不均导致无法获得资源而无法执行的情况。
虚拟内存与物理内存的联系与区别
基础概念
还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:
- 因为我的物理内存是有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
- 由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据
- 因为内存是随机分配的,所以程序运行的地址也是不正确的。
解决方法:虚拟内存
一个进程运行时都会得到4G的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。
进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为的),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
进程访问一个地址,经历的过程
- 每次要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
- 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
- 进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
- 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
- 当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
- 缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法的设计了。
虚拟内存和物理内存的区别和联系
页表的工作原理
- 我们的cpu想访问虚拟地址所在的虚拟页(VP3),根据页表,找出页表中第三条的值判断有效位。 如果有效位为1,DRMA缓存命中,根据物理页号,找到物理页当中的内容,返回。
- 若有效位为0,参数缺页异常,调用内核缺页异常处理程序。内核通过页面置换算法选择一个页面作为被覆盖的页面,将该页的内容刷新到磁盘空间当中。然后把VP3映射的磁盘文件缓存到该物理页上面。然后页表中第三条,有效位变成1,第二部分存储上了可以对应物理内存页的地址的内容。
- 缺页异常处理完毕后,返回中断前的指令,重新执行,此时缓存命中,执行1。
- 将找到的内容映射到高速缓存当中,CPU从高速缓存中获取该值,结束。
虚拟内存是怎么工作的
- 当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
- 在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
- 可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)
虚拟内存机制的优点
- 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,交给内核来完成映射关系
- 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
- 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存是连续的,实际上,往往物理内存都是断断续续的内存碎片。可以有效地利用我们的物理内存
信号量和事件的区别
- 用途:
- 信号量:信号量主要用于控制多个线程或进程对共享资源的访问,以限制并发访问的数量。信号量通常用于解决资源分配和互斥访问的问题。
- 事件:事件主要用于线程或进程间的通知和同步,以通知一个或多个线程某个事件的发生,然后等待其他线程响应或等待事件的发生。
- 计数:
- 信号量:信号量是一个计数器,它可以是任意非负整数。信号量有两种操作:增加(通常称为"V操作")和减少(通常称为"P操作")。P操作会使信号量减一,而V操作会使信号量加一。
- 事件:事件通常是二进制的,只有两种状态:有信号和无信号。事件可以设置为有信号或无信号状态,线程可以等待事件的状态发生变化。
- 等待机制:
- 信号量:线程可以等待信号量的值达到某个特定的值或等待信号量变为非零。信号量的等待是基于计数的。
- 事件:线程等待事件的状态,通常是等待事件变为有信号状态。事件的等待是基于事件的状态的改变。
- 触发方式:
- 信号量:信号量的值可以通过V操作或P操作来改变,通常由其他线程来改变。
- 事件:事件的状态通常由某个线程或进程显式设置为有信号状态,以触发其他等待的线程。
- 常见应用:
- 信号量:常见的应用包括限制并发访问资源,例如控制访问临界区、控制线程池的并发任务数量等。
- 事件:常见的应用包括线程间的通知,例如生产者-消费者问题、等待多个线程完成某个任务等
event事件和信号Signal的区别
:::info
信号和事件在某种程度上是相似的,它们都用于线程或进程之间的通信和同步,但它们有一些重要的区别。
事件(Event):事件通常是一种明确的通知机制,用于一个线程通知其他线程或进程某个特定的事件已经发生。事件通常有两个状态,有信号和无信号,通常使用信号量、互斥锁或条件变量等机制来实现。线程可以等待事件的状态发生变化,然后响应这个事件。事件通常用于协调多个线程之间的操作,例如生产者-消费者问题中,生产者线程通知消费者线程有数据可用。
信号(Signal):信号通常是一种异步通知机制,它可以被操作系统或其他进程发送给目标进程或线程。信号是一种突发性的通知,它不需要接收方线程或进程的明确等待。当某个事件发生时,可以发送信号给接收方,接收方将中断当前操作来处理信号。信号通常用于处理异步事件,例如处理外部硬件中断、处理异常或错误等。
虽然事件和信号都用于通信和同步,但它们的使用方式和语义略有不同。事件更适用于线程之间的协作和明确的通知,而信号更适用于异步事件的处理。在不同的情境下,你可以选择使用事件或信号来满足你的需求。
:::
定时器的实现方法
- 基于线程的定时器:使用线程来实现定时器功能。在这种方法中,可以创建一个专门的线程,该线程循环检查当前时间和定时任务的到期时间,并执行到期的任务。可以使用操作系统提供的定时器相关的API(如
sleep
、usleep
等)来控制线程的等待时间,以达到定时的效果。
优点:
- 简单易用,不需要过多的特殊处理。
- 可以实现较高的精度。
缺点: - 需要额外的线程来执行定时任务,增加了系统资源的消耗。
- 在多线程环境下,需要进行线程同步和互斥操作,以确保定时任务的正确执行。
- 基于事件驱动的定时器:使用事件驱动的方式实现定时器功能。在这种方法中,可以使用事件循环(Event Loop)机制,将定时任务注册为特定的事件,并设置相应的定时时间。当时间达到时,事件循环会触发相应的事件回调函数,执行定时任务。
优点:
- 资源消耗较低,不需要额外的线程。
- 可以和其他事件(如IO事件)结合,实现异步和非阻塞的定时任务处理。
缺点: - 实现较为复杂,需要对事件循环机制和事件驱动编程有一定的了解。
- 精度可能受到事件循环的调度和处理效率的影响。
选择哪种定时器实现方法取决于具体的需求和应用场景。如果对定时器的精度要求较高,或需要处理大量的定时任务,可以考虑使用基于线程的定时器。而如果希望实现高效的异步和非阻塞定时任务处理,可以选择基于事件驱动的定时器。
多线程和多进程的怎么选择
- 并发性:多线程适合处理并发性高的任务,因为线程之间共享相同的内存空间,可以更方便地进行数据共享和通信。多进程适合处理相对独立的任务,进程之间拥有独立的内存空间,需要通过进程间通信机制进行数据交换。
- 资源消耗:多线程的资源消耗较少,因为线程之间共享内存和其他系统资源。多进程的资源消耗较大,因为每个进程都需要独立的内存空间和系统资源。
- 可扩展性:多线程相对于多进程更容易扩展,因为线程的创建和销毁开销较小,且线程间的通信更加高效。多进程的扩展性较差,因为创建和销毁进程的开销较大,进程间通信相对复杂。
- 安全性:多进程相对于多线程更安全,因为进程之间拥有独立的内存空间,一个进程的错误不会影响其他进程。多线程在共享内存的情况下,需要额外的同步机制来保证数据的一致性和安全性。
- 开发复杂度:多线程相对于多进程开发更简单,因为线程间的数据共享和通信更为方便,线程的创建和销毁开销较小。多进程开发相对复杂,需要处理进程间通信和同步的问题。
在需要高并发、共享数据和较小资源消耗的情况下,多线程是一个不错的选择
在需要处理独立任务、更高安全性和更好的扩展性的情况下,多进程可能更适合。
进程异常崩溃,日志来不及写,怎么办?
- 使用缓冲区:在进程中使用缓冲区来存储日志信息,而不是直接将日志写入文件。通过将日志信息暂时存储在缓冲区中,可以提高写入速度。当缓冲区满或达到一定条件时,再将缓冲区中的日志批量写入文件。这样可以减少频繁的磁盘写入操作,提高性能。
- 异步日志:使用异步日志的方式,将日志的写入操作交给专门的线程来处理,而不阻塞主线程的执行。主线程将日志信息放入一个队列中,异步日志线程从队列中读取日志并写入文件。这样可以将日志写入的操作与主线程的执行解耦,提高性能和稳定性。
- 优化日志写入方式:对于频繁的日志写入操作,可以考虑优化日志写入的方式。例如,可以将日志写入到内存文件系统或者使用高性能的日志库。内存文件系统具有较高的写入速度,而高性能的日志库可以提供更快的日志写入操作。
- 使用进程监控工具:使用进程监控工具来监测进程的状态和异常情况。当进程异常崩溃时,监控工具可以及时捕获异常,并记录相关信息。这样可以在进程崩溃后,通过监控工具提供的信息进行分析和排查问题。
压测?怎么继续优化,改进性能?
压力测试(Load Testing)是评估系统在负载条件下的性能表现的一种方法。在进行压力测试后,如果希望继续优化和改进系统的性能,可以考虑以下几个方面:
- 性能分析:通过性能分析工具,如性能监测器(profiler)、日志分析工具等,对系统进行详细的性能分析。识别瓶颈和性能瓶颈所在的模块或代码片段。
- 代码优化:根据性能分析的结果,对性能瓶颈的代码进行优化。这可能包括改进算法、减少不必要的计算、优化数据库查询、缓存数据等。优化代码的目标是减少系统资源的消耗、提高运行效率。
- 并发处理:如果系统是多线程或多进程的,可以考虑优化并发处理。这包括减少线程之间的竞争和同步、提高并行度、优化锁的使用、使用线程池等。合理的并发处理可以提高系统的吞吐量和响应性能。
- 数据库优化:对于数据库相关的性能问题,可以考虑优化数据库的设计和查询操作。这包括合理的索引设计、查询优化、使用数据库缓存、合理拆分和分区等。数据库优化可以显著提升系统的性能。
- 缓存优化:使用缓存可以减少对底层资源的访问,提高系统的响应速度。考虑使用适当的缓存策略,如内存缓存、分布式缓存等。合理的缓存机制可以减轻系统负载并提高性能。
- 垃圾回收和资源管理:对于使用自动垃圾回收的语言,合理管理垃圾回收和资源释放是重要的。确保垃圾回收不会造成明显的性能问题,并避免资源泄漏。
- 水平扩展和负载均衡:如果系统面临高负载情况,可以考虑进行水平扩展和负载均衡。将系统分布在多台服务器上,通过负载均衡将请求均匀分发,提高系统的吞吐量和可伸缩性。
进程调度
进程调度是操作系统中的一个重要功能,用于决定哪些进程应该在处理器上运行。操作系统通过进程调度算法从就绪队列中选择一个进程,并将其分配给可用的处理器执行。进程调度的目标通常是提高系统的性能和资源利用率,以及提供良好的响应时间和公平的资源分配。
- 先来先服务(FCFS)调度:按照进程到达的顺序进行调度,先到达的进程先执行。
- 短作业优先(SJF)调度:选择估计执行时间最短的进程进行调度,以减少平均等待时间。
3.** 优先级调度**:为每个进程分配一个优先级,根据优先级的高低决定进程的执行顺序。 - 时间片轮转(Round Robin)调度:每个进程被分配一个固定的时间片,在时间片用完之后,进程被挂起,下一个进程被调度执行。
- 多级反馈队列调度:根据进程的优先级和运行时间将进程划分为多个队列,不同队列采用不同的调度算法,例如时间片轮转和优先级调度的结合。
这些调度算法各有优缺点,适用于不同的场景和需求。操作系统通常会根据具体的系统特点和性能需求选择合适的调度算法。此外,现代操作系统还可能采用一些高级调度策略,如多核调度、负载均衡和实时调度等,以满足更复杂的系统需求。
LRU了解过吗?
LRU(Least Recently Used,最近最少使用)是一种页面置换算法,用于操作系统中的虚拟内存管理,而不是进程调度算法。虚拟内存管理是操作系统中负责将进程的虚拟地址映射到物理内存地址的一部分。
LRU算法的主要思想是基于页面的访问模式,它假设最近被访问过的页面很可能在未来会被再次访问。当物理内存不足时,操作系统需要决定将哪些页面从内存中置换出去,以便为新的页面腾出空间。LRU算法选择最久未被访问的页面进行置换。
线程同步的方式
1.** 互斥锁**(Mutex):互斥锁是一种最基本的线程同步机制。通过对共享资源加锁,只允许一个线程访问共享资源,其他线程需要等待锁释放后才能访问。互斥锁可以防止多个线程同时修改共享数据,保证数据的一致性和线程安全。
2. 信号量(Semaphore):信号量是一种用于控制线程并发访问的同步原语。它通过一个计数器来管理资源的访问数量。当计数器大于0时,允许线程访问资源;当计数器为0时,线程需要等待其他线程释放资源后才能继续访问。信号量可以用于控制资源的并发访问量,例如限制同时访问的线程数量。
3. 条件变量(Condition Variable):条件变量用于线程间的等待和通知机制。一个线程可以通过条件变量等待某个条件满足,而另一个线程可以通过条件变量发出信号来通知等待的线程条件已满足。条件变量通常与互斥锁结合使用,等待线程在获取互斥锁后等待条件变量,而通知线程在修改共享数据后发送信号通知等待的线程。
4. 屏障(Barrier):屏障是一种同步机制,用于等待一组线程都到达某个点后再继续执行。当所有线程都到达屏障时,屏障解除,线程可以继续执行下一步操作。屏障通常用于需要多个线程协同完成的任务,确保所有线程都完成了前置操作后再进行后续操作。
5. 读写锁(Read-Write Lock):读写锁允许多个线程同时对共享资源进行读操作,但只允许一个线程进行写操作。可以提高读操作的并发性能,需要保证写操作的互斥性。读写锁适用于读操作远远多于写操作的场景。
虚拟内存了解吗?
- 虚拟内存是一种操作系统提供的抽象层,将进程所使用的内存空间抽象为连续的虚拟地址空间,而不必直接依赖于物理内存的实际可用性。虚拟内存的主要目的是扩大进程的地址空间,并提供了一些额外的功能,如内存保护、内存共享和更高级别的内存管理。
- 虚拟内存通过将进程的虚拟地址映射到物理内存上的实际地址来实现。这个映射关系是由操作系统的内存管理单元(MMU)负责管理的。当进程访问虚拟地址时,MMU将其转换为对应的物理地址,然后访问实际的物理内存。
- 在虚拟内存中,内存被分割为固定大小的页(Page),而物理内存也被分割为相同大小的物理页帧(Page Frame)。虚拟内存的页和物理内存的页帧之间建立映射关系,这样进程可以按页来访问内存。
- 虚拟内存的一个重要概念是页面置换(Page Replacement),当物理内存不足时,操作系统会将一部分不常用的页面置换到辅助存储设备(如硬盘)上,从而腾出物理内存空间供其他页面使用。当进程再次访问被置换出去的页面时,操作系统会将其重新调入内存。
- 虚拟内存的另一个重要功能是内存保护和权限管理。通过给每个页面设置不同的权限位,操作系统可以限制进程对内存的访问权限,从而提高系统的安全性和稳定性。
- 虚拟内存是操作系统提供的一种抽象机制,它为进程提供了一个连续的虚拟地址空间,并通过页表映射将虚拟地址转换为物理地址。虚拟内存可以扩大进程的地址空间,提供内存保护和权限管理,以及支持页面置换等功能。
open怎么直接写硬盘
在C++中,使用标准库函数 open
来直接写硬盘是不太可能的,因为 open
函数主要用于打开文件而不是直接写硬盘。
硬盘是由操作系统管理的存储设备,一般情况下,需要通过文件系统来访问和操作硬盘上的数据。文件系统提供了一种组织和管理硬盘数据的机制,包括文件的创建、读取、写入和删除等操作。
如果你想要直接对硬盘进行读写操作,需要使用操作系统提供的底层接口或设备驱动程序来进行操作。这种操作通常需要特权级别的权限,并且需要对硬盘的结构和格式有深入的了解。
在Linux系统中,可以使用 /dev/
目录下的设备文件来进行直接的硬盘访问。例如,可以通过打开 /dev/sda
或 /dev/hda
文件来访问第一个硬盘,然后使用底层的读写函数(如 read
和 write
)来进行读写操作。但这样的操作非常底层,需要对硬盘的数据结构和文件系统有深入的了解,并且需要小心操作,以避免对数据造成损坏。
需要注意的是,直接对硬盘进行操作是一项高级任务,需要谨慎处理,并确保了解相关的硬件和操作系统细节。如果没有足够的经验和理解,建议谨慎使用,并确保备份重要的数据。
轮询,中断,DMA的各自特点和优缺点
轮询(Polling),中断(Interrupt),和直接内存访问(Direct Memory Access,DMA)是常用的输入/输出(I/O)操作方式,各自具有不同的特点和优缺点。下面是它们的概述:
轮询(Polling):
特点:
- I/O设备状态的查询是由CPU通过不断轮询的方式进行的。
- CPU需要主动查询设备的状态以确定是否有数据可用。
- 轮询是同步的方式,CPU必须等待设备的响应。
优点: - 简单直观,易于实现。
- 适用于低速设备或数据传输量较小的场景。
- 控制权完全在CPU手中,可以精确控制I/O操作。
缺点: - CPU会浪费大量时间在轮询上,降低了CPU的利用率。
- 实时性较差,需要等待设备的响应,造成延迟。
- 频繁的轮询可能消耗大量的能量。
中断(Interrupt):
特点:
- 当I/O设备准备好数据时,会发送一个中断信号给CPU,中断处理器会暂停当前的工作,转而处理中断请求。
- 中断请求可以异步地打断CPU的执行,使得CPU能够及时响应设备的请求。
优点: - 提高了系统的实时性,CPU可以及时响应设备的请求。
- 减少了CPU的空闲时间,提高了CPU的利用率。
- 可以处理多个设备的请求,实现了设备的并行操作。
缺点: - 中断处理过程需要一定的开销,包括中断响应时间和中断处理程序的执行时间。
- 需要额外的硬件支持来处理中断请求,增加了系统的复杂性。
直接内存访问(Direct Memory Access,DMA):
特点:
- DMA是一种数据传输机制,允许外设直接与内存进行数据传输,而不需要通过CPU的直接参与。
- DMA控制器负责管理数据传输的过程,从外设读取或写入数据,然后直接传输到内存中。
优点: - 提高了数据传输的效率,减少了CPU的负担。
- 允许CPU与DMA并行工作,提高了系统的并发性。
- 适用于大数据块的高速传输,如音频、视频等。
缺点: - 需要额外的硬件支持,包括DMA控制器和DMA通道。
- DMA的配置和管理相对复杂,需要仔细处理共享资源和同步问题。
- DMA可能会影响系统的实时性,特别是在数据传输期间。
物理内存和虚拟内存的关系?
物理内存是计算机实际存在的内存硬件,是指计算机中实际可供程序使用的内存空间。它由物理内存条组成,通常是RAM(随机存取存储器)。
虚拟内存是一种由操作系统提供的抽象概念,它扩展了物理内存的概念。虚拟内存是一种将磁盘空间作为补充内存的机制,它允许程序使用比物理内存更大的内存空间。
在使用虚拟内存的系统中,每个程序都被分配了一块连续的虚拟内存空间。这个虚拟内存空间通常比实际可用的物理内存空间大得多。虚拟内存空间被划分为固定大小的页面(或称为页),与物理内存中的页面对应。
当程序需要访问虚拟内存中的某个页面时,操作系统将其映射到物理内存中的一个页面。这种映射关系是动态的,可以根据需要在物理内存和磁盘之间进行调度。操作系统根据程序的访问模式和需要释放的内存空间来管理虚拟内存和物理内存之间的转换。
虚拟内存的主要作用是:
- 扩展可用内存:虚拟内存使得程序可以使用比物理内存更大的内存空间,允许运行更大型的程序。
- 内存管理:虚拟内存允许操作系统将内存分页,按需加载和卸载页面,提供更高效的内存管理。
- 内存保护:虚拟内存为每个程序提供了独立的地址空间,保护了每个程序的内存不被其他程序非法访问。
总结起来,虚拟内存是对物理内存的一种扩展和抽象,通过动态映射和管理,使得程序可以使用比物理内存更大的内存空间,并提供了内存管理和保护的功能。
逻辑地址到物理地址?
逻辑地址是由CPU生成的地址,它是相对于进程而言的虚拟地址。逻辑地址空间是给每个进程独立分配的地址空间,每个进程都认为自己在独立的地址空间内执行。逻辑地址通常是以连续的方式编号,从0开始。
物理地址是指计算机内存中实际的地址,它是RAM中存储单元的物理位置。物理地址空间是实际的硬件地址范围,它表示计算机系统可用的实际内存空间。
逻辑地址和物理地址之间的关系是通过内存管理单元(MMU)来实现的。MMU是CPU中的一个硬件模块,负责地址转换和内存访问权限的管理。
当程序访问逻辑地址时,MMU会将逻辑地址转换为对应的物理地址。这个地址转换过程是通过使用页表或段表等数据结构来实现的。页表或段表中存储了逻辑地址和物理地址之间的映射关系。
逻辑地址转换为物理地址的过程包括以下几个步骤:
- 程序使用逻辑地址访问内存。
- MMU根据页表或段表查找逻辑地址和物理地址的映射关系。
- 如果映射存在,MMU将逻辑地址转换为对应的物理地址。
- 程序使用物理地址访问内存,完成内存读取或写入操作。
逻辑地址和物理地址之间的关系可以理解为逻辑地址是程序视图下的虚拟地址,而物理地址是实际硬件存储单元的地址。MMU的作用是将逻辑地址转换为物理地址,实现程序与硬件之间的映射和访问。这种逻辑地址到物理地址的转换机制使得每个进程可以独立运行,互相隔离,提高了计算机系统的安全性和可靠性。
父进程和子进程的区别
- 创建关系:
- 父进程是通过操作系统启动一个新的进程来创建子进程。通常,父进程会调用特定的系统调用(例如
fork()
)来创建子进程。 - 子进程是由父进程创建的,子进程是在父进程的执行环境中复制父进程的状态和资源。子进程和父进程在创建时的状态是一样的,但是它们拥有不同的进程ID(PID)
2.** 执行独立性**: - 父进程和子进程是并发执行的,它们在独立的执行环境中运行。子进程有自己的代码和数据空间,可以执行自己的任务,而不会受到父进程的干扰。
- 子进程可以执行与父进程完全不同的代码路径,可以根据需要执行不同的操作。
- 父进程是通过操作系统启动一个新的进程来创建子进程。通常,父进程会调用特定的系统调用(例如
- 进程关系:
- 子进程通常是父进程的一个副本,包括代码、数据和打开的文件描述符等。子进程可以访问父进程的资源,如文件、管道等。
- 子进程和父进程之间通常是独立的,它们有自己的内存空间和系统资源。子进程可以修改自己的状态和资源,而不会影响父进程或其他子进程。
- 进程ID:
- 父进程和子进程拥有不同的进程ID(PID)。父进程的PID是在创建子进程时分配的,而子进程的PID是通过调用
fork()
系统调用从父进程继承的。
-** 子进程的PID通常是父进程的PID加上一个唯一的标识符**,用于在进程表中进行标识和管理。
- 父进程和子进程拥有不同的进程ID(PID)。父进程的PID是在创建子进程时分配的,而子进程的PID是通过调用
- 进程间通信:
- 父进程和子进程之间可以通过各种进程间通信机制进行通信和数据交换,例如管道、消息队列、共享内存等。
- 进程间通信机制可以使父进程和子进程之间进行数据传递和协调工作,实现进程间的协作和同步。
进程和线程
- 资源占用:每个进程都有独立的内存空间和系统资源(如文件描述符、网络连接等),而线程是在进程内共享这些资源的。因此,创建和切换线程的开销比创建和切换进程的开销要小。
2**. 执行方式**:进程是独立执行的任务单位,拥有独立的执行序列。一个进程可以包含多个线程,它们共享进程的资源,可以同时执行不同的任务。 - 调度:线程是调度的基本单位,而进程是系统进行资源分配和调度的基本单位。线程由操作系统内核进行调度,而进程之间的调度由操作系统进行。
- 通信和同步:进程之间通信比较复杂,需要使用操作系统提供的机制,如管道、共享内存、消息队列等。而线程之间共享进程的内存空间,可以直接通过读写共享变量来进行通信。此外,线程之间的同步较为容易,可以使用互斥锁、条件变量等同步机制,而进程之间的同步需要更复杂的同步机制。
- 容错性:由于线程共享进程的资源,一个线程的错误可能会影响到同一进程中的其他线程。而进程之间相互独立,一个进程的错误不会直接影响到其他进程。
死锁产生的条件是什么?Cpp 中如何避免死锁?
死锁是多线程或多进程环境中的一种资源竞争问题,它发生在两个或多个进程(线程)互相等待对方持有的资源而无法继续执行的情况。死锁产生的条件通常被称为死锁四个必要条件,包括:
- 互斥条件(Mutual Exclusion):一个资源一次只能被一个进程(线程)持有,如果一个进程(线程)已经获得了某个资源,其他进程(线程)必须等待。
- 请求与保持条件(Hold and Wait):一个进程(线程)在持有某个资源的同时还可以请求其他资源,而不释放自己已经持有的资源。
- 不可剥夺条件(No Preemption):已经分配给一个进程(线程)的资源不能被强制性地剥夺,只能由该进程自行释放。
- 循环等待条件(Circular Wait):存在一个进程(线程)的资源请求链,使得每个进程(线程)都在等待下一个进程(线程)所持有的资源。
为了避免死锁,可以采取以下策略: - 避免使用多个锁:减少系统中使用的锁的数量,或将多个锁合并为一个更大的锁,从而降低发生死锁的可能性
- 统一加锁顺序:对于需要同时获取多个锁的情况,确保线程以相同的顺序获取锁,避免不同线程之间出现循环等待的情况。
3.** 加锁超时机制**:为锁设置超时时间,当一个线程尝试获取锁时,如果在一定时间内未成功获取锁,就放弃获取,释放已经持有的锁,并重新尝试获取锁,以避免无限等待。 - 死锁检测与恢复:实现死锁检测机制,定期检查系统中是否存在死锁,并在检测到死锁时采取恢复措施,例如释放某些资源或终止某些进程(线程)。
- 避免循环等待:通过对资源进行编号,要求线程按照编号的升序请求资源,从而避免循环等待的条件。
- 合理规划资源分配:通过合理的资源分配策略,避免系统中出现资源竞争的情况,从而减少死锁的可能性。
在 C++ 中,可以使用以下机制来避免死锁:
- RAII(Resource Acquisition Is Initialization):RAII 是一种资源管理的技术,通过对象的生命周期管理资源的获取和释放。使用 RAII 可以确保在离开作用域时,资源会被正确释放,避免因为忘记释放资源而导致死锁的发生。
- 避免嵌套锁:尽量避免在持有一个锁的情况下再去请求另一个锁,因为这样容易造成死锁。如果确实需要嵌套锁,要保证加锁的顺序是一致的,即以相同的顺序获取和释放锁。
- 使用互斥锁而非递归锁:递归锁允许同一个线程多次获取同一个锁,这可能导致死锁。因此,为了避免死锁,尽量使用互斥锁(非递归锁)来进行资源的互斥访问。
- 使用死锁检测机制:在复杂的系统中,死锁可能不易被完全避免,因此可以使用死锁检测机制来及时发现死锁的存在。常用的死锁检测算法有资源分配图算法(Resource Allocation Graph)和银行家算法(Banker’s Algorithm)。
- 合理规划资源分配:在设计系统时,合理规划资源的分配和使用,避免资源的浪费和竞争,从而降低死锁的风险。
- 使用同步原语的正确性保证:当使用信号量、条件变量等同步原语时,确保对它们的使用符合正确的设计原则,避免在使用时出现逻辑错误导致死锁。
- 使用并发编程库:使用经过充分测试和验证的并发编程库,这些库通常提供了安全的同步机制和资源管理工具,可以帮助减少死锁的发生。
线程会有自己独立的栈区吗?会有独立的堆区吗?
- 线程在操作系统中会拥有独立的栈区,用于存储局部变量、函数调用信息以及其他与线程执行相关的数据。每个线程都会分配一块独立的栈空间,用于支持其运行时的函数调用和局部变量的存储。当线程创建时,操作系统会为其分配栈空间,并在线程执行过程中动态管理栈的大小。
- 至于堆区,则是进程级别的共享资源,不会为每个线程分配独立的堆区。堆是用于动态分配内存的区域,线程可以通过堆来进行动态内存分配,例如使用malloc()、new等函数来申请内存。多个线程可以在堆上进行内存分配和释放操作,因此需要采取同步机制(如互斥锁)来保护共享资源的访问。
你了解 Linux 虚拟内存空间吗?
在Linux系统中,每个进程都有自己的虚拟内存空间,它将进程看作是在独占的地址空间中运行,提供了一种抽象机制,使每个进程感觉自己独立使用系统的全部内存。
Linux的虚拟内存空间通常被划分为以下几个部分:
- 用户空间(User Space):这是进程用于执行用户代码和存储用户数据的区域。它通常从0开始,高地址部分是共享库和堆区,低地址部分是代码段、数据段和栈区。
- 内核空间(Kernel Space):这是操作系统内核运行的区域,用于执行内核代码和管理系统资源。进程无法直接访问内核空间,必须通过系统调用接口来请求内核提供服务。
- 共享库区(Shared Libraries):这是存放共享库的区域,多个进程可以共享同一个物理内存上的共享库,以减少内存占用和提高系统性能。
- 堆区(Heap):这是动态分配内存的区域,用于存储运行时动态分配的内存块,例如使用malloc()或new函数分配的内存。
- 栈区(Stack):每个线程都有自己的栈区,用于存储局部变量、函数调用信息和其他与线程执行相关的数据。栈是按照先进后出(LIFO)的顺序进行管理。
- 内存映射区(Memory Mapped Region):它允许将文件映射到进程的地址空间,使得文件可以像内存一样进行读写操作。这对于处理大文件或共享内存非常有用。
Linux的虚拟内存管理使用了分页和分段的技术,通过虚拟内存机制,操作系统可以为每个进程提供相对较大的虚拟地址空间,同时实现了内存的保护和隔离。
虚拟内存有什么好处?
虚拟内存提供了一种抽象的、扩展的内存模型,使得系统能够高效地管理内存资源,并提供了隔离、保护、共享和灵活性等多个好处,从而提高了计算机系统的性能、稳定性和可靠性。
- 扩展内存容量:虚拟内存允许进程使用比物理内存更大的地址空间。它通过将不常用的数据存储在辅助存储设备(如硬盘)上,并在需要时进行动态调度,从而扩展了可用的内存容量。这使得运行大型程序和处理大量数据成为可能。
- 内存隔离和保护:虚拟内存为每个进程提供了独立的地址空间,使得每个进程在逻辑上感觉自己拥有整个系统的内存。这样可以实现进程之间的内存隔离,一个进程的错误不会直接影响其他进程的稳定性。此外,虚拟内存还提供了内存保护机制,防止进程越界访问内存区域。
- 共享代码和数据:虚拟内存使得多个进程可以共享同一个物理内存中的代码和数据,例如共享库。这样可以减少内存占用,提高系统性能,并简化代码和数据的更新和维护。
- 惰性加载和写回策略:虚拟内存使用惰性加载(Lazy Loading)策略,只有当进程访问某个内存页面时,才将该页面加载到物理内存中。这样可以减少启动时间和内存占用。此外,虚拟内存还使用写回(Write-back)策略,即将修改过的页面暂时保存在缓存中,只有在需要释放内存或页面被替换时才将其写回到辅助存储设备上,提高了系统的性能和效率。
- 内存管理的灵活性:虚拟内存使得内存管理更加灵活,可以根据需要进行动态分配和回收内存。进程可以动态地请求和释放内存,而无需预先知道可用的物理内存大小。这种动态管理使得系统可以更好地适应不同进程的需求和变化。
两个进程 malloc 可能会返回一个值吗?会映射到一个物理地址吗?
可能,不会
- 在多进程环境中,每个进程都有自己独立的虚拟内存空间。因此,当两个不同的进程调用malloc()函数进行内存分配时,它们将在各自的虚拟内存空间中获得不同的地址。malloc()函数返回的指针值是相对于各自进程的虚拟地址空间的偏移量,并且在每个进程中都是唯一的。
- 尽管两个进程的指针值可能相同(例如,两个进程的malloc()都返回0x1000),但这些值在物理内存中是映射到不同的物理地址的。由于虚拟内存的存在,进程使用的是虚拟地址,而不是直接访问物理地址。
- 操作系统通过页表机制将进程的虚拟地址映射到物理内存上的物理地址。每个进程都有自己的页表,它将虚拟地址分割成固定大小的页面,并将这些页面映射到物理内存中的物理页面。不同的进程具有不同的页表,因此它们的虚拟地址到物理地址的映射是独立的。
- 综上所述,尽管两个进程的指针值可能相同,但它们在各自的虚拟地址空间中是唯一的,且映射到不同的物理地址上。这是通过虚拟内存和页表机制实现的,确保了进程之间的地址空间隔离和内存的独立性。
如果两个进程同时尝试向同一个文件(例如1.txt)写入数据,会乱吗?
那么由于并发性和竞争条件的存在,最终的结果可能会发生乱序或者混乱。
- 进程1和进程2同时访问文件1.txt,它们可能会交错地写入数据,导致文件中的内容变得混乱。例如,结果可能是"AABABABAAA"或"BBABABBABA",具体取决于进程之间的调度顺序。
- 两个进程都尝试同时写入文件1.txt,可能会发生竞争条件。这可能导致一些数据丢失或覆盖,结果可能是部分内容丢失,例如"AAABBB"或"BBBAAA"。
为了避免这种情况,需要使用同步机制来确保同时只有一个进程能够访问文件。例如,可以使用文件锁或其他并发控制机制,以确保在某一时刻只有一个进程能够写入文件。这样可以避免数据的混乱和竞争条件的发生。
进程与线程的区别
进程是资源分配的最单位,线程是调度的最小单位
进程具有自己的独立地址空间,线程共享进程的地址空间
一个进程可以包含多个线程,线程之间的通信比进程之间通信更便捷
临界区和互斥量的区别
1)临界区是一段代码或一段程序区域,在这个区域的操作可能会被多个并发执行的线程或进程访问,临界区的实现方式有互斥量,信号量,条件变量等
2)互斥量是一种特殊的数据结构,用于线程同步,保护临界区
信号量和锁的区别是什么?
如果需要对多个资源进行控制,或者希望实现一定数量的资源分配,那么信号量可能是更适合的选择。如果只需对单一资源进行互斥访问,那么锁可能更为简单直接
进程之间的通信
管道:半双工,具有父子关系的进程之间通信,pipe创建
命名管道:允许无亲缘关系的进程之间的通信,通过特殊文件FIFO实现
信号量:资源的一种计数器,进程可以通过对信号量进行操作来实现对资源的争用,确保只有一个进程能够访问共享资源,通过semget系统调用
信号:异步通信机制,通知进程发生某一事件,可以注册信号处理函数来响应信号,中断,kill命令可以用来发生信号
消息队列:一个进程写入队列,一个进程读,系统调用
socket:主要用于不同机器上之间的通信
共享内存:最快,通过映射共享内存区域到它们的地址空间来访问共享数据
文件锁:通过再文件上上锁来同步
进程之间通信(IPC)的好处
1)模块化设计
2)更好的并发和并行,提高系统的性能
3)可以进行分布式计算
4)更好的任务分工
共享内存用过吗
通常通过将同一块内存区域映射到多个进程的地址空间来实现。这样,不同进程可以访问和操作共享内存中的数据,实现高效的数据交换
//写入进程
#include <iostream>
//包含第三方库
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>int main() {// 创建共享内存int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);if (shmid == -1) {perror("shmget");return 1;}// 将共享内存映射到进程的地址空间char* shared_memory = (char*)shmat(shmid, NULL, 0);if (shared_memory == (char*)-1) {perror("shmat");return 1;}// 写入数据到共享内存strcpy(shared_memory, "Hello, shared memory!");// 解除共享内存映射shmdt(shared_memory);return 0;
}//读取进程
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>int main() {// 获取共享内存的标识符int shmid = shmget(IPC_PRIVATE, 1024, 0666);if (shmid == -1) {perror("shmget");return 1;}// 将共享内存映射到进程的地址空间char* shared_memory = (char*)shmat(shmid, NULL, 0);if (shared_memory == (char*)-1) {perror("shmat");return 1;}// 读取共享内存中的数据std::cout << "Received: " << shared_memory << std::endl;// 解除共享内存映射shmdt(shared_memory);// 删除共享内存shmctl(shmid, IPC_RMID, NULL);return 0;
}
线程之间通信的方式
管道:一个线程写,一个线程读
消息队列:一个线程发送到队列,一个从队列接收
信号量:是一种计数器,用于控制对临界区的访问。用于限制同时访问某一资源的线程数量。线程可以通过执行 P(等待)和 V(释放)操作来操作信号量
共享内存:允许多个线程访问相同的内存区域
锁机制:任意时刻只有一个线程能够访问进入临界区,如果没得到锁,会阻塞等待锁释放
条件变量:线程之间的通知与等待,常与互斥锁一起用,一个线程等待某个条件的成立,另一个线程通过信号去唤醒另一个等待线程
屏障:同步多个线程,知道所有线程到达,接着执行,对于分阶段任务很有用
线程池:管理线程的机制,有任务队列数,可以实现任务的分发和重用
死锁产生的四个必要条件
死锁形容:
鼓和鼓锤被放在不同的玩具箱里, 并且两个孩子在同一时间里都想要去敲鼓。 之后, 他们就去玩具箱里面找这个鼓。 其中一个找到了鼓, 并且另外一个找到了鼓锤
只要系统发送死锁,以下条件必然成立
1)互斥条件:一个资源每次只能被一个进程使用
2)请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
怎么防止死锁
1)**避免嵌套锁,**一个线程已获得一个锁时, 再别去获取第二个
2)避免在持有锁时调用用户提供的代码
3)使用固定顺序获取锁
4)使用锁层次
CPU不能直接访问的存储器
可以直接访问ROM只读存储器,RAM随机存储器,Cache高速缓存存储器,不能直接访问外存,如SSD固态硬盘
大小端具体场景
大多数网络协议使用大端字节序
特定硬件和嵌入式系统可能使用小端字节序
线程创建方式,你常用的是哪个,为什么
pthread(POSIX),嵌入式编程,对性能要求较高的,跨平台性能好
thread(c++11),跨平台性能差一点
线程的五种状态——以及wait和sleep和中断的区别
1)新建
2)就绪
3)运行
4)阻塞
5)终止
wait用于线程之间的协作,sleep用于线程的延时和时间控制,中断用于线程的请求式终止
进程和线程的区别
定义
进程:程序的执行实例,资源分配的基本单位,拥有独立的内存空间,文件描述符,运行时的堆栈,每个进程都是独立运行的,都有自己的程序计数器和上下文
线程:进程中的执行单元,一个进程可以包含多个线程,线程共享进程的资源,包括内存空间,文件描述符等,线程之间可以并发的执行,共享同一进程的上下文
资源占用
进程:进程之间的资源不能直接共享,需要通过进程间通信IPC机制来进行数据交换
线程:线程共享进程的资源,包括内存空间,打开的文件,网络连接等,线程间可以直接访问共享的内存,通信更加高效
创建和销毁
进程:需要较大的系统开销,包括分配独立的内存空间和建立上下文
线程:开销相对较小,因为共享了进程的资源
调度和上下文切换
进程:操作系统以进程为单位进行调度,将处理器分配给不同的进程,进程的切换需要保存和恢复进程的上下文信息,包括寄存器状态、程序计数器等
线程:调度的基本单位, 操作系统可以将处理器分配给不同的线程。线程切换只需要保存和恢复线程的上下文,开销比进程切换小。
i = i + 1大概执行多久
在编译型语言(如C++)中,这个表达式通常会被编译器优化为一个简单的指令,例如递增指令。执行这样的指令只需要很少的时钟周期。
在解释型语言(如Python)中,执行这个表达式可能会涉及一些额外的操作,如类型检查和动态内存分配。因此,相对于编译型语言,执行时间可能稍微长一些,但仍然非常短暂。
解释性语言和编译性语言差别
解释性语言:逐行解释源代码,并将其转换为可执行的机器指令。解释器在运行时逐行执行代码,每次执行一条语句。解释性语言通常会将源代码逐行翻译成中间代码或者解释执行,而不会生成可执行的二进制文件。
编译型语言:在运行之前需要经过编译过程,将源代码转换为与特定硬件平台兼容的机器代码(可执行文件),然后再由计算机直接执行。
解释性语言的优点:
- 简单易学:解释性语言通常语法简洁、易于理解和学习。
- 跨平台性:解释性语言不依赖于特定的硬件平台,可以在不同的操作系统上运行。
- 动态性:解释性语言通常支持动态类型,灵活性较高,可以在运行时修改代码。
解释性语言的包括:
- 执行效率较低:相比编译型语言,解释性语言的执行速度通常较慢,每次执行都需要解释器逐行翻译代码。
- 难以保护源代码:由于解释性语言的源代码直接可见,相对容易被他人阅读和修改。
了解中断吗
大白话:就是我现在正在干一件事A,然后另外一件事情B插入,干完另外一件事B在回来干A
定义:中断是由硬件或软件产生的事件,用于打断正在执行的程序,将控制权转移到相应的中断处理程序, 中断可能来自硬件设备(如键盘、鼠标、定时器等)的信号,可能是由软件生成的信号(如系统调用、异常等)
1)中断请求:硬件或软件发出中断信号,请求处理器进行中断处理
2)中断响应:处理器接收到中断请求,暂停当前正在执行的指令,保存当前的执行状态
3)中断处理:跳转到中断处理程序的入口点,执行相应的中断处理代码,更新设备状态,进行数据处理
4)中断返回:中断处理程序执行完毕后,处理器将控制权返回给被中断的程序,继续执行被中断的指令
中断使计算机系统能够实现异步事件处理,提高了系统的可响应性和效率。不同的中断有不同的优先级,系统会根据优先级来确定中断的处理顺序。操作系统和硬件设备需要相互配合,以确保中断的正确处理和协调。
中断处理程序需要尽量保持简洁和高效,因为中断会打断正在执行的程序,频繁的中断处理可能会对系统的性能产生影响。
键盘上敲一个字母是什么中断
硬件中断, 具体的中断号和处理方式可能因操作系统和硬件平台而异
同步异步
同步:代码就是从上执行到下,必须执行完上一句,才能执行下一句。上一句没有执行完,则一直在等待(阻塞)
异步:我不等你了,我先做自己的事。你上一句执行完了,就告诉我一声,我再干跟你相关的事。这种一般用在网络上,因为网络上状态就很多变,如果我为了拿一个数据,一直让用户等待,数据重要就还好,数据不重要这就很bug了。
非阻塞IO与阻塞IO的区别
阻塞 I/O:
- 当进行阻塞 I/O 操作时,应用程序会一直等待,直到请求的 I/O 操作完成。
- 阻塞 I/O 操作会导致应用程序在等待 I/O 完成期间无法执行其他任务,因此阻塞 I/O 是同步的。
- 当进行阻塞 I/O 操作时,操作系统会将应用程序置于睡眠状态,直到 I/O 操作完成。
- 阻塞 I/O 的常见示例是调用
**read()
或recv()**
函数来读取数据,这些函数将会阻塞等待数据的到达。
非阻塞 I/O: - 当进行非阻塞 I/O 操作时,应用程序发起 I/O 请求后,会立即返回并继续执行其他任务,而不会等待 I/O 操作的完成。
- 非阻塞 I/O 操作会立即返回一个状态,表示 I/O 操作是否已完成。如果 I/O 操作尚未完成,应用程序可以选择继续执行其他任务,或者再次尝试 I/O 操作。
- 非阻塞 I/O 操作通常需要通过轮询或异步回调来检查 I/O 状态,并在数据可用时进行读取或写入。
- 非阻塞 I/O 的常见示例是调用
select()
、poll()
、epoll()
等函数进行 I/O 多路复用,或使用异步 I/O 模型。
选择使用阻塞 I/O 还是非阻塞 I/O 取决于应用程序的需求和设计。阻塞 I/O 可以简化编程模型,但在处理多个连接或需要同时处理其他任务时可能导致性能问题。非阻塞 I/O 可以提高并发性和响应性,但编程模型更复杂一些。在实际应用中,通常会根据具体的场景和需求来选择合适的 I/O 模型。