操作系统
1.死锁:面向进程
允许多个进程并发执行共享系统资源时,系统必须提供同步机制和进程通信机制。
四个必要条件:互斥条件、请求和保持条件、不可剥夺条件、环路等待条件。
解决方法:资源一次性分配、资源可剥夺、资源按序分配等。
给进程分配的资源:
用于存放程序正文、数据的磁盘空间和物理内存空间,以及运行时所需要的I/O设备,已打开的文件,信号量等。
进程调度算法:
先来先服务、短作业优先、高优先权优先(抢占式、非抢占式、动态优先权)、时间片轮转、多级反馈队列调度。
线程是轻量级的进程,那么线程的调度策略应该和进程的调度策略相同。
内核调度的对象是线程,不是进程。
在多核系统中,操作系统会尽量保证进程只被同一个核调度,因为被多核调度的话,需要迁移进程,那么就需要迁移进程上下文,会带来额外的开销。
pid: 进程编号,操作系统分配,用于识别进程。
进程的时间片的长度由进程数目、切换开销、系统效率和响应时间等多方面因素来综合考量。
进程通信方式:管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
动态/静态就绪、阻塞
GPU与CPU:
1)二者的线程概念是相同的。GPU的SM数量限制了能够同时运行的线程的数目,CPU的Core数量限制了能够同时运行的线程的数目。
图像来源:《CUDA_C_Programming_Guide》
2)GPU采用了数量众多的计算单元,但只有非常简单的控制逻辑。而CPU不仅被Cache占据了大量空间,而且还有复杂的控制逻辑和诸多优化电路,相比之下计算能力只是CPU的一部分。
二者的设计用途不同,分别针对了两种不同的应用场景。CPU需要很强的通用性来处理各种不同的数据类型,同时逻辑判断又会引入大量的分支跳转和中断的处理,这些都使得CPU的内部结构异常复杂。而GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。因此GPU主要用于大规模并发计算,且最好是相互之间独立的运算。
进程所能创建的线程的数目:
CPU的核心数限制了同一时刻只能有相同数量的线程并行执行,但是并不意味着只能创建相同数量的线程。当创建的线程数目大于CPU的核心数时,操作系统会通过线程调度来使得每个线程都会被执行。
进程所能创建的线程数目与进程拥有的栈空间大小、系统参数限制有关。
可通过减少分配给线程的栈空间大小,来增加所能创建的线程的数目。但是,减少了线程的栈空间,线程所能存储的数据(例如临时变量)便会减少。
2.多个进程的并行执行:
MMU:内存管理单元,用于将虚拟地址转换为物理地址。也用于内存保护,可以设置内存区域的访问权限,如只读、可读写、不可访问等。
对于多核CPU,如果只有一个MMU,则不能并行执行多个进程,只能并行执行同一个进程的多个线程。
若每个CPU的核心都有一个MMU,则可以并行执行多个进程。因为此时每个MMU维护自己的页表。
虽然不同进程之间通过维护各自的页表使得虚拟地址空间相互独立(除了少数情况,如共享内存),但那仅限于用户部分,而内核部分的地址映射对于所有进程都是相同的。也就是说,页表中内核部分的映射对于所有进程来说都是相同的,因为对于所有进程来说,内核空间是共享的。
实际上,进程的执行表现为线程的执行。
进程切换的时机:1)进程正常结束,主动放弃。2)进程在执行时发生了异常。3) 进程在执行时,系统产生了外部中断。4)有更高优先级的进程。5) 分配给进程的时间片到期。
3.多线程模型主要优势为线程间切换代价较小,因此适用于I/O密集型的工作场景,因为I/O密集型的工作场景经常会由于I/O阻塞导致频繁的线程切换。同时,多线程模型也适用于单机多核分布式场景。
多进程模型,适用于CPU密集型。同时,多进程模型也适用于多机分布式场景中,易于多机扩展。
协程的特点在于是一个线程执行。
如果物理内存被用完了呢?
用硬盘,比如 windows 系统的分页文件,就是把一部分虚拟内存放到了硬盘上。相应的,此时程序运行会很慢,因为硬盘的读写速度比内存慢很多,是我们可以感受到的慢,这就是为什么开多了程序电脑就会变卡的原因。
线程崩溃,所属进程不一定会被终止
每个进程是独立的,进程 A 出问题不会影响到进程 B;虽然线程也是独立运行的,但是一个进程里的线程是共用同一个堆,如果某个线程 out of memory,那么这个进程里所有的线程都会异常终止。如果线程因为访问内存异常而终止,那么一旦另一个线程也访问了同一块内存,则此线程也会终止运行。
线程池:将多个线程对象放到一个容器当中。
作用:可以重用线程,减少创建和销毁线程带来的消耗。
线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
自增、自减操作不是线程安全的,因为并不是仅由一条指令便可完成执行。
四核八线程
其实质就是采用超线程技术同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个只能阻塞,直到这些资源闲置后才能获取这些资源。因此超线程的性能并不等于两颗CPU的性能。
4.线程共享的资源
线程私有资源:所属线程的栈区、程序计数器、栈指针以及函数运行使用的寄存器是线程私有的。统称为线程上下文。
进程切换上下文:
一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
进程表:进程(程序)开始运行时,由Linux系统调用自己的系统函数,在内存中开辟task_struct结构体,又叫进程表。 task_struct的数据成员主要有:进程状态、内核栈信息、进程使用状态、PID、优先级、锁、时间片、队列、信号量、内存管理信息、文件列表等等与进程管理、调度密切相关的信息。
5.键盘输入一个字符到显示在显示器上的全过程
当用户输入了键盘字符之后,键盘控制器会产生扫描码数据,并将其缓冲在键盘控制器的缓冲区中。然后键盘控制器通过总线向CPU发送中断请求。CPU收到中断请求后,操作系统会保存被中断进程的CPU上下文,然后调用键盘的中断处理程序。
键盘的中断处理程序是在键盘驱动程序初始化时注册的,那键盘中断处理函数的功能就是从键盘控制器的缓冲区读取扫描码,再根据扫描码找到用户在键盘输入的字符,如果输入的字符是显示字符,那就会把扫描码翻译成对应显示字符的 ASCII 码,比如用户在键盘输入的是字母 A,是显示字符,于是就会把扫描码翻译成 A 字符的 ASCII 码。
得到了显示字符的 ASCII 码后,就会把 ASCII 码放到「读缓冲区队列」,显示设备的驱动程序会定时从「读缓冲区队列」读取数据放到「写缓冲区队列」,最后把「写缓冲区队列」的数据一个一个写入到显示设备的控制器的数据缓冲区,最后将这些数据显示在屏幕里。
显示出结果后,恢复被中断进程的上下文。
6.信号量本质是计数器,用来负责数据操作过程中的同步、互斥等功能,当一个进程不再使用资源时,信号量+1(对应的操作称为V操作),反之当有进程使用资源时,信号量-1(对应的操作为P操作),PV操作都是原子操作;P操作时,如果信号量的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行;V操作时,如果有其他进程因等待信号量而被挂起,就让它恢复运行,如果没有进程因等待信号量而挂起,就给它加1。
7.计算机的启动过程
CPU开始运行之后,第一步就是取地址0xFFFFFFF0处的指令(CPU 设计时固化的功能)。CPU拿到指令后执行,发现这条指令是告知自己,下一次去另一个地址取指,即开始执行BIOS(Basic Input/Output System)程序。
8.32位和64位的区别
1)32位程序和64位程序的 long、unsigned long、指针所占的字节数不同。前者占据32位,即4字节。但后者占据64位,即8字节。
2)之所以叫做“64位处理器”,是因为电脑内部都是实行2进制运算,处理器(CPU)一次处理数据的能力也是2的倍数。所以64位处理器的处理速度会更快。
3)64bit CPU拥有更大的寻址能力,最大支持到16GB内存(理论值应该是2^64byte,但是实际寻址能力只能达到16GB),而32bit只支持4GB内存(32位表示 2^32 个地址,而每一个地址指向的是 8bit为一组的 byte)。
9.伙伴算法(link)(link)
将内存分成若干块,然后尽可能以最适合的方式满足程序内存需求的一种内存管理算法,伙伴算法的一大优势是它能够完全避免外部碎片的产生。申请时,伙伴算法会给程序分配一个较大的内存空间,即保证所有大块内存都能得到满足。很明显分配比需求还大的内存空间,会产生内部碎片。所以伙伴算法虽然能够完全避免外部碎片的产生,但这恰恰是以产生内部碎片为代价的。
外部碎片:是指还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。外部碎片是除了任何已分配区域或页面外部的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。
内部碎片:当一个进程装入到固定大小的分区块(比如页)时,假如进程所需空间小于分区块,则分区块的剩余的空间将无法被系统使用。
10.操作系统是管理计算机硬件与软件资源的计算机程序。(link)
功能主要是进程管理、设备管理、存储管理、文件管理、作业管理(负责处理用户提交的请求)。
12.如果操作系统执行系统调用,陷入内核态,来了一个中断信号,操作系统怎么处理?
优先级较高的中断优先于优先级较低的中断。
13.操作系统的内存管理方式有块式、段式、页式内存管理(link)。
Linux采用段式、页式相结合的方式,但实际上段式只是为了兼容硬件(link,可先看总结)。
TLB:缓存虚拟地址和其映射的物理地址,即负责缓存最近常被访问的页表项,大大提高了地址的转换速度。因为对于多级页表(时间换取空间)而言,查找一个映射,需要一级一级的查找,比较耗时。
14.对于物理内存来说,所有地址均是相同的,不存在内存分区。内存分区仅是逻辑上的分区,便于管理内存单元,将不同的地址段划分为不同的分区。所以内存分区是针对虚拟内存而言的。
比如堆、栈,堆内存是操作系统抽象的,而栈由CPU直接提供支持。从硬件上,堆和栈都是内存条上若干存储单元,并没有什么不同。 但是很多CPU对压栈出栈有指令上的支持,所以栈区分配/归还内存速度极快(相比之下,堆极慢)。尤其在局部变量上,可以轻易地与函数调用/返回绑定,因此几乎所有编程语言都利用栈来管理局部变量。
因此二者物理上相同,堆是系统抽象的。但是栈内存访问速度快。
15.可执行文件的执行过程
1)通过系统调用创建一个新的进程;
2)建立可执行文件与虚拟内存之间的映射,初始化进程环境;
3)execve()调用启动代码,启动代码将调用main()函数,当启动代码将main()函数的虚拟地址传递给CPU时,CPU通过解析虚拟地址发现内存中没有main()相对应的页或者物理块,然后CPU通过进程中的页表项找到可执行文件所在的磁盘位置,将磁盘上的块拷贝到内存中(即缺页中断),之后CPU执行可执行文件。
16.缓存(Cache)与缓冲(buffer)的区别:
Cache: 是在读取硬盘中的数据时,把最常用的数据保存在内存的缓存区中,再次读取该数据时,就不去硬盘中读取了,而在缓存中读取。
buffer: 是在向硬盘写入数据时,先把数据放入缓冲区,然后再一起向硬盘写入,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。
缓存(cache)是用来加速数据从硬盘中"读取"的,而缓冲(buffer)是用来加速数据"写入"硬盘的。
page cache中有一部分磁盘文件的缓存,因为从磁盘中读取文件比较慢,所以读取文件先去page cache中去查找,如果命中,则不需要去磁盘中读取,大大加快读取速度。
17.使用多线程来提高程序处理速度,其本质是提高对CPU的利用率。但是线程切换是有开销的,如果多线程带来的收益大于开销,那么适合使用多线程。否则,多线程程序的运行速度反而会更慢(link)。
18.数据总线、地址总线、控制总线(link)
数据总线的宽度决定CPU与其他元器件一次最大传送的数据量;
地址总线的宽度决定CPU的寻址能力;
控制总线决定CPU对其他元器件的控制能力。
19.段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、修改只读内存区域的数据等。例如栈溢出、多次delete同一对象等。
发生段错误,程序退出的原因(link):
段错误,由内核识别并且通过信号SIGSEGV通知用户进程。处理该信号的默认行为是: “Core Default action is to terminate the process and dump core”。所以,如果程序中不捕获信号SIGSEGV,那么其默认的处理方式是:中止程序并dump一份程序结束时的状态信息!当然,程序员可以在程序中捕获该信号,简单的可以用signal()注册一个SIGSEGV的处理函数,那么当发生SIGSEGV的时候,就会调用注册的信号处理函数了,而不是执行默认的信号处理操作!
对信号的捕获和处理,是在内核中进行的!
进程对于信号的处理方式:默认方式、自定义信号处理函数、忽略该信号。
信号本质:软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
20.fork()、vfork()、写时复制
当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。
返回值:
负数:如果出错,则fork()返回-1,此时没有创建新的进程,最初的进程仍然运行。
零:在子进程中,fork()返回0。
正数:在父进程中,fork()返回子进程的PID。
21.虚拟内存
通过使用虚拟内存技术,保证各个进程地址空间相互独立而互不影响。
给进程分配的虚拟内存大小一般是硬件所能支持的内存大小,但是真正能使用的物理内存的大小并没有那么大,因为内核需要使用一部分内存,系统也要使用一部分内存来记录进程的一些信息,比如进程的入口等,此外,其他一些系统资源也需要使用一部分物理内存。
22.缺页中断
23.页表寻址
24.页面置换算法:FIFO、LFU、LRU、LRU-K等。
25.大端存储模式:数据的低字节部分存储在内存的高位地址,数据的高位字节部分存储在内存的低位地址。
小端存储模式则相反。
26.源代码到可执行文件
27.零拷贝
没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA(直接内存访问,Direct Memory Access) 来进行传输的。零拷贝技术相比于传统的文件传输方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,都是通过 DMA 来执行。
29.内存池的作用
1)减少外部内存碎片。
2)new/delete操作,需要与内核进行交互。使用内存池,可以减少与内核的交互次数,提升效率。
动态内存分配算法
首次适应、循环首次适应、最佳适应、最坏适应算法。
First Fit、Next Fit、Best Fit、Worst Fit.
30.地址重定位
操作系统将逻辑地址转变为物理地址的过程。分为静态重定位与动态重定位。
静态重定位是在程序执行之前进行重定位,它根据装配模块将要装入的内存起始位置,直接修改装配模块中的使用地址的指令。
动态重定位是指,在程序执行过程中进行地址重定位。更确切地说,是在CPU每次访问内存单元前才进行地址变换。动态重定位可使装配模块不加任何修改便可装入内存,但是它需要额外的硬件支持——寄存器。
31.跨模块释放内存
在Linux中,每个进程只有一个heap,在任何一个动态库模块中通过new或者malloc来分配内存的时候都是从这个唯一的heap中分配的,那么在其它随便什么地方都可以释放。
在Windows中,一个进程存在着多个heap,除了一个主heap外,还有很多的__crtheap,用来处理通过C、C++的运行库进行的内存操作。所以使用new/malloc来分配的内存实际上都是局部的,可以在多个dll中共享,但必须由申请者释放对应的内存(link)。
在Windows中,MT模块的内存,不能跨模块释放。但是MD模块的内存,可以跨模块释放(link)。
Visual Studio 编译选项MT、MTd、MD、MDd。
32.CPU如何区分指令和数据(二者均以二进制格式存放)
1)通过不同的时间段来区分指令和数据,即在取指令阶段(或取指微程序)取出的为指令,在执行指令阶段(或相应微程序)取出的即为数据。
2)通过地址来源区分,由PC提供存储单元地址的,取出的是指令,由指令地址码部分提供存储单元地址的,取出的是操作数。