1. 描述多道批处理、分时、实时操作系统的特点各是什么?
多道批处理系统:系统可同时容纳多个作业,但是不允许用户与其作业产生交互。
- 系统利用率高。在内存中驻留了多道程序,共享资源,资源得到充分利用
- 系统吞吐量大。1. CPU和资源都处于“忙碌”;2. 只有作业完成或运行不下去,才进行切换,开销小
- 平均周转时间长。周转时间指:从作业进入系统到完成并退出系统的总时间。因为作业需要排队、依次处理,所以周转时间长
- 无交互能力
分时系统:系统按照时间片运行作业,并且每个用户可以通过自己的终端向系统发出操作控制命令。
- 有较好的人机交互的特性,并且可以实现共享主机
实时系统:保证能在一定时间内完成特定功能的操作系统。
- 在特定时间内完成特定的内容,实时性和可靠性
2. 请分别简单说一说进程和线程以及它们的区别。
进程 | 线程 | |
定义 | 可以独立运行的程序在某个数据集上的一次运行活动 |
进程中的一个执行路径 系统内核态,更轻量的进程 |
角色 |
系统资源分配的单元 | 系统调度的单元 |
组成部分 | 各个进程拥有自己独立的资源。由数据、程序、PCB组成 |
线程共享所在进程的地址空间和其他资源。 只拥有一些必须资源:栈、栈指针、程序计数器等寄存器——>线程切换代价小 |
独立性 | 进程有自己独立的地址空间 |
线程没有自己独立的地址空间。 线程必须依赖进程而存在 |
3. 进程与程序的区别
1. 程序是静态概念,进程是动态概念, 是可以独立运行的程序在某个数据集上的一次运行
2. 不同的进程可以包含同一程序,只要该程序所对应的数据集不同。
3. 进程具有并行特征,而程序不反映执行所以没有并行特征
4. 进程是竞争计算机系统资源的基本单位,而程序不反映执行也就不会竞争计算机系统资源
4. 多进程和多线程的区别
一. 应用:
多进程:是指计算机中多个进程并发运行,例如,边浏览网站,边听歌。
多线程:是指一个进程内,多个执行部分并发执行。例如:打开QQ,和多个人聊天。
二、从进程和线程的性质来说:
进程:是可独立运行的程序在某个数据集上的一次运行结果,是资源分配的基本单位,每个进程有自己独立的地址空间,进程之间不共享内存。
线程:是进程中的一个执行路径。没有独立的地址空间,多个线程共享进程的地址空间,是调度的基本单元。
所以:
1. 进程创建、销毁效率>线程。
在需要频繁的创建、销毁情景下,多使用线程。在线程中有线程池来控制线程的数量,实现线程复用,降低线程使用成本。
例如:Web服务器,创建一个连接则创建一个线程。
2. 需要大量计算——切换
所谓大量计算,要消耗很多CPU、切换频繁,这种情况下适合多线程
eg:图像处理、算法处理
3. 进程之间不共享内存,而在一个进程内多个进程共享线程的内存——通信方式不同
- 进程:管道、IPC、socket
- 线程:互斥量、信号机制、事件
4. 可靠性
- 多进程:进程间互不影响,一个进程挂掉不会导致真个系统崩溃
- 多线程:一个线程挂掉将导致整个进程挂掉。
进程 |
线程 |
|
数据共享 |
进程拥有独立的地址空间,数据隔离,同步简单 共享内存难,进程间通信机制:管道、IPC、SOCKET |
多个线程共享同一个进程的地址空间和资源,同一进程内共享资源简单 但是同步困难,线程中通信机制:互斥量、信号量、事件机制 在不同进程间,线程的通信和进程一致 |
成本 |
创建销毁、切换复杂,速度慢 |
轻巧的上下文切换开销——不用切换地址空间,不用更改CR3寄存器,不用清空TLB。
|
内存 | 占用内存多, | 占用内存少 |
可靠性 | 进程间互不影响,一个进程挂掉不会导致真个系统崩溃 | 一个线程挂掉将导致整个进程挂掉 |
使用场景 |
1. 弱相关 2. 多机分布 |
1. 需要频繁的创建、销毁(使用线程池) 2. 需要大量计算。 3. 强相关 4. 多核分布 |
5. 你有用过多线程吗?
多线程是指:对一个进程内可执行部分的并发操作,同时提高了代码的复用性。
在Python里面由GIL全句解释锁和thread、threading、queue等模块实现。
1. GIL全局解释锁
保证在同一时刻,只有一个线程在解释器中运行。
在进行线程切换时:
- 首先,设置GIL锁
- 切换到一个线程去运行
- 当运行了指定的字节码指令或线程主动退出,则把该线程设置为sleep状态,解锁GIL
此外,在调用外部代码时,GIL也会被锁定,直到这个函数被完成
2. 比较thread、threading、queue
首先,thread和threading都允许程序员创建和管理线程。
- thread:提供了基本的线程和锁的支持。当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清理工作。
- threading:能确保重要的子线程退出后进程才退出。
- Queue模块:允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
7. 进程的通信方式有哪些?
Q1:管道与文件描述符、文件指针的关系?
内核利用文件描述符来访问文件 。
管道本质上就是一个文件,能使用read、write、open等普通I/O操作,所以类似于文件描述符。
事实上, 管道使用的描述符、文件指针和文件描述符最终都会转化成系统中SOCKET描述符,都受到系统内核中SOCKET描述符的限制.。
Q2 管道 VS IPC
Q3 进程间通信和线程间通信的关系?
因为Windows运行的实体就是线程,狭义上来说进程的通信也就是不同进程的线程之间通信。单个进程内部的线程同步问题也是一种特殊的进程通信。实现线程同步,有三种方法:互斥量(mutext)、信号量、事件机制(通知)。
8. 说一说进程同步有哪几种机制。
进程间同步的主要方法有原子操作、信号量机制、自旋锁、管程、会合、分布式系统等。
9. 线程同步的方式有哪些?
1. 互斥量(锁):采用互斥对象机制,只有拥有互斥对象(锁)的线程才拥有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源在同一时间不会被多个线程同时访问。
2. 信号量(PV操作):它允许多个线程在同一时间访问同一资源,通过一个计数器来控制同一时刻访问该资源的数目。
- 这个计数器初始值为资源的个数,
- 每当一个进程申请一个资源(P操作),则计数器减一;当这个计数器<0,表示已经没有资源了,所有等待进程都放到一个队列等待,计数器的绝对值为等待进程的个数。
- 每当一个进程完成,则释放一个资源(V操作),计数器+1, 如果此时计数器仍<0,则唤醒一个进程。
3. 事件机制(信号):通过通知操作方式来保持多线程同步,还可以方便的实现多线程优先级的比较。
具体而言,就是允许一个线程处理完一个任务后,主动唤醒另一个线程执行任务。
例如:A线程负责侦听端口,线程B负责更新用户数据,利用事件机制,则线程A可以通知B更新数据
10. 进程同步与互斥
1. 互斥:在同一时刻,只允许一个进程访问这个共享资源,即进入临界区。在互斥中没有规定线程是否有序。——无序。例如:过独木桥
2. 同步:仍然是对共享资源的访问,指多个进程按照约定的顺序,因为制约而相互配合、相互等待。——有序
例如:消费者和生产者共享一个区域,生产者只有在消费者腾出位置后才能够生产,消费者只能在生产者生产出东西后才能够购买。
3. 互斥发生的五条基本原则:
有空让进、忙则等待、多种选一、有限等待、让权等待(即:等待的进程主动放弃CPU)
11. 什么是临界区,如何解决冲突?
临界区:每个进程中访问临界资源的那段程序,每次只允许一个进程进入临界区,进入后其他进程不准进入。
解决冲突——互斥
(1)有空则进:如果有临界区空闲,则进程可以进入
(2)多种选一:每次只能个选择一个进程进入。
(2)忙则等待:临界区任何情况下只能有一个进程进入;如果有一个进程进入临界区,则其他进程只能等待
(3)有限等待:进入临界区的进程在有限时间内必须退出,以便其他进程能够访问
(4)让权等待:如果进程不能进入临界区,则让入CPU,避免出现“忙等”
12. 进程有哪几种状态?
1)进程有五种状态:新建、就绪、运行、阻塞、死亡;另外还有挂起态
1. 当用fork()新建一个进程,则进入新建状态
2. 当进程已经获得除CPU之外的所需资源,则进入就绪状态,等待分配CPU资源;
3. 当获得CPU资源,其程序正在处理机上执行,进入运行状态
4. 正在执行的程序,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。
5. 正在执行的程序,由于时间片用完(中断),则进入就绪状态
6. 当阻塞的进程,等到事件发生了,则进入就绪状态
7. 当内存中没有就绪状态的进程,则将阻塞进程置换道磁盘,进入
挂起状态,在此之后,系统将一个阻塞状态的进程调入内存进入就绪状态或者接受一个新进程的请求
13. 线程的状态变化
线程有五大状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。
1. 新建状态:用new创建一个线程,此时还没有在其上调用start()方法。例如:new Thread(r)
2. 就绪状态:当线程有资格运行,在等待调度程序分配处理机的状态。
3. 运行状态:获得处理机,在运行的状态
4. 阻塞状态:正在运行的线程没有运行结束,暂时让出CPU,使得其他处于就绪状态的线程能获得CPU进入运行状态。
例如:
- 线程通过sleep方法进入阻塞状态
- 等待I/O输出完成
- 线程试图得到一个锁,但是这个锁被其他线程所持有
- 线程在等待某个触发事件
5. 死亡状态
有两种方法导致线程死亡:
1)run()方法正常完成——自然死亡
2)一个未捕获的异常终止了run()方法,而使线程死亡。
死亡的线程无法复活,虽然他可能还有生命力,但是已经无法单独执行了。
可以用isAlive()查看是否存活:如果处于阻塞/运行状态:true;如果处于新建/死亡:false
对死去的线程调用start()方法会报错:java.lang.IllegalThreadStateException
14. 操作系统中进程调度策略有哪几种?
1. 先来先服务(FCFS, First Come First Serve)
既可用于作业调度,也可用于进程调度——选择最先进入队列的作业/进程
1)用于作业调度:从后备作业队列中选择一个或多个最先来的作业,调入内存,分配资源、创建进程,放入就绪队列
2)用于进程调度:从后续队列中选择最先进入队列的进程,分配处理机,使之投入运行状态。
2. 短作业优先(SJF, Shortest Job First)
选择运行时间最作业/进程
3. 最高优先权调度(Priority Scheduling)
1)非抢占式优先权算法
2)抢占是优先权调度算法
但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程
4. 最高响应比优先调度算法
(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。
简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
5. 时间片轮转(RR, Round Robin)
系统将进程按照先来先服务的原则排成一个队列,每次调度时,把处理机非配给队首进程,执行一个时间片以后,由计时器发出一个中断来停止进程,并将它送至队尾。
6. 多级反馈队列调度(multilevel feedback queue scheduling)
(1)设计多个等待队列,为每个队列赋予不同的优先级别。第一个队列的优先级别最高,之后各队列优先权逐步下降。赋予每个队列的时间片也不一样,第一个队列时间片最小,之后各队列时间片逐步增加。
(2)当一个新进程进入内存后,首先放入第一个队列末尾,按照FCFS的原则等待调度。若它在该时间片内能够完成,则可准备撤离系统;如果在该时间片没有完成,则进入第二个队列的末尾,按照FCFS原则等待调度。如果它在第二个队列仍没有完成,则进入第三个队列。。。如此下去,直到完成。
(3)尽当第一个队列空闲时,才会调度第二个队列中的进程;仅当第1~i-1个队列都空闲时,才能调度第i个队列。如果在调度第i个队列某个进程时,在1~i-1个队列中又有新进程进入,则这个新进程会抢占正在运行的进程。——最抢占式高优先权调度。
实时调度算法:
对于实时系统:保证在一定的时间内完成特定的功能——实时性、可靠性
1. 最早截至时间优先 EDF
2. 最低松弛度优先 LLF
松弛度:截止时间-运行时间
例如:一个进程它必须在400ms时完成,本身需要运行150ml,则它的松弛度:400-150=250
15. 什么是死锁?死锁产生的条件?解决死锁的方法?
另外:
活锁:两个或两个以上的进程为了响应其他进程中的变化而持续改变自己的状态但不做有用的工作
饥饿:指一个可运行的线程尽管能继续执行,但被调度程序无限期地忽略,而不能调度执行。
在编程上防止死锁
(1)请勿尝试在可能会对性能造成不良影响的长时间操作(如 I/O)中持有锁;
(2)请勿在可能直接或间接递归调用自己的函数里持有锁;
(3)一般情况下,请先使用粗粒度锁定方法,确定瓶颈,并在必要时添加细粒度锁定来缓解瓶颈。大多数锁定都是短期持有,而且很少出现争用。因此,请仅修复测得争用的那些锁定;
(4)使用多个锁定时,通过确保所有线程都按相同的顺序获取锁定来避免死锁;
(5)在等待某个资源时,使用超时机制;
(6)在系统中使用一个定时检查死锁环的机制,如果发现死锁就让一个线程的资源释放掉(数据库好像就是用这种方式)。
16. 操作系统的内存管理
一、分页、分段、段页式
1)分页:
1. 定义:用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。
2. 大小:由硬件决定。例如:32bit系统:4k,64bit系统:8k
- 页面太小:页面小可以减少内存碎片,但是当每个程序占用太多的页面时,页表过长,占用大量的内存,并且降低页面换进换出的效率。
- 页面太大 :页面浪费大、产生很多页面碎片。
2. 页表:系统为每个进程创建了一个页面:实现从页号到物理块的对应。
3. 地址空间:若给定一个逻辑地址为A,页面大小为L,则页号P=INT[A/L],页内地址W=A MOD L
4. 快表:分页系统中,CPU每次要存取一个数据,都要两次访问内存(访问页表、访问实际物理地址)。为提高地址变换速度,增设一个具有并行查询能力的特殊高速缓冲存储器,称为“联想存储器”或“快表”,存放当前访问的页表项。
2)分段:
1. 定义:将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。
2. 地址空间:每个段都从0开始编址,并采用一段连续的地址空间。因为每段长度不同,所以地址表示为:(段号,段内位移量)
3)段页式存储管理
分页系统能有效地提高内存的利用率,而分段系统能反映程序的逻辑结构,便于段的共享与保护,将分页与分段两种存储方式结合起来,就形成了段页式存储管理方式。
在段页式系统中,作业的地址空闲首先被划分为若干个逻辑分段,每段都有自己的段号,然后再将段分成若干个相等的页。对于主存空间,也分城大小相等的页,主存的分配以页为单位。
地址空间:三维:(段号:页号:页内位移量)
4)比较分段和分页:
分页 | 分段 | |
目的 |
页是信息的物理单位, 是为了实现非连续性,以解决内存碎片的问题,是系统管理需要 |
段是程序的逻辑单元,它含有一组意义相对完整的信息 目的是:实现共享和保护,满足用户需要 |
大小 | 大小固定,由机器硬件实现(32位:4k;64位:8k) |
长度却不固定,决定于用户所编写的程序。 通常由编译程序在对源程序进行编译时根据信息的性质来划分. |
地址空间 | 一维 | 二维(段号:相对位移) |
二、程序的内存管理:
1.代码段:存放着程序的机器码和只读数据,可执行指令就是从这里读取的。多进程中,可共享代码段,但是一般被标记为只读,任何对这个区域的写操作会导致段报错。
2. 数据段:包括已初始化的数据段(.data)和未初始化的数据段(.bss),前者用来存放保存全局的和静态的已初始化变量,后者用来保存全局的和静态的未初始化变量。数据段在编译时分配。
3. 堆:程序运行时动态分配的内存段。
堆的大小并不固定,可扩张或缩减。其分配由:malloc、new等这类实时分配函数实现。
- 当程序调用malloc()分配内存时,新分配的内存就被动态的添加到堆上(堆扩展)。
- 当利用free()释放内存时,被释放的内存从堆中剔除掉(堆缩减)。堆的释放应该由应用程序控制,通常一个new()就要对应一个delete(),如果程序员没有释放掉,那么在程序结束后操作系统会自动回收。
4. 栈:用来存储函数调用时的临时信息结构,例如:函数的局部变量、传递的参数、返回值等。
在程序运行时,由编译器在需要的时候分配,在不需要的时候自动清除。
栈的特征:先进后出。
堆 | 栈 | |
分配管理方式 |
动态分配,例如:malloc()、new()等事实分配函数实现 一般来说,释放是由应用程序管理,例如:使用一个new(),对应一个delete()释放,如果程序员没有释放,则由操作系统来完成 |
由编译器自动管理,一般分为:静态分配和动态分配: 1. 静态分配:由编译器完成,如局部变量的分配 2. 动态分配:由alloca()函数进行分配,但是由编译器管理。 |
产生碎片 | 频繁的new/delete或者malloc/free势必会造成内存空间的不连续,造成大量的碎片,使程序效率降低。 | 不存在碎片问题,因为栈是先进后出的队列,永远不可能有一个内存块从栈中间弹出。 |
生长方向 | 从内存的低地址向高地址方向增长。 | 由内存的高地址向低地址方向增长 |
(二)对于Python语言,是通过内存池、引用计数、垃圾回收进行管理的
1. 引用计数
Python是动态语言,即在使用前不需要声明,对内存地址的分配是在程序运行时自动判断变量类型并赋值。
对每一个对象,都维护着一个指向改对象的引用的计数,obj_refcnt
当变量引用一个对象,则obj_refcnt+1,如果引用它的变量被删除,则obj_refcnt-1,系统会自动维护这个计数器,当其变成0时,被回收。
2. 内存池
内存池与C不同的是——分配和释放小内存块
- 如果请求分配的内存在1~256字节之间就使用自己的内存管理系统pymalloc,否则直接使用 malloc。这里还是会调用 malloc 分配内存,但每次会分配一块大小为256k的大块内存。
- 经由内存池登记的内存,最后被回收到内存池,而不会调用C的free释放掉,以便下次使用。
- 对于Python对象,如数值、字符串、元组,都有其独立的私有内存池,对象间不共享他们的内存池。例如:如果分配又释放了一个大的整数,那么这块内存不能再被分配给浮点数。
3. 垃圾回收
PythonGC主要是通过引用计数来跟踪和回收垃圾,并使用“标记-清理”技术来解决容器对象可能产生的循环引用问题,使用“分代回收”提高回收的效率。
1). 引用计数:在Python中存储每个对象的引用计数obj_refcnt。当这个对象有新的引用,则计数+1,当引用它的对象被删除,则计数-1,当技术为0时,则删除该对象。
- 优点:简单、实时性
- 缺点:维护引用奇数消耗资源、无法解决循环引用的问题。
2). 标记-清理:基本思路是按需分配。等到没有空闲时,从寄存器和程序栈中的引用出发,遍历以对象为节点,引用为边所构成的图,把所有可以访问到对象都打上标记,然后清扫一遍内存空间,把没有标记的对象释放。
3.) 分代收集:
总体的思想:Python将系统中所有的内存块根据对象存活时间划分为不同的代(默认是三代),垃圾收集频率随着“代”的存活时间的增大而减小。举例:
我们分为新生代、老年代、永久代。
在对对象进行分配时,首先分配到新生代。大部分的GC也是在新生代发生。
如果新生代的某内存M进过了N次GC以后还存在,则进入老年代,老年代的GC频率远低于新生代。
对于永久代,GC频率特别低。
17. 虚拟内存技术
虚拟存储器是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储系统。
18. 页面置换算法
1. 定义:
- 在地址映射中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。
- 当发生缺页中断,如果操作系统内存中没有空闲页面,则操作系统必须在内存中选择一个页面将其移除内存,以便为即将调入的页面让出空间。
- 用来选择淘汰哪一个页面的规则叫做页面置换算法。
2. 常见的置换算法:
1)最佳置换算法OPT
每个页面都由:该页面首次访问前所要执行的指令数进行标记。当缺页发生时,系统将拥有最大指令数的页面置换出。
该算法无法实现,因为操作系统无法知道各页面下一次在什么时候被访问
但是这个算法可以用于对可实现算法的性能进行衡量比较。
2)先进先出置换算法(FIFO)
3)最近最久未使用(LRU)算法
可以把过去最长一段时间里不曾被使用的页面置换掉
4)最少使用(LFU)置换算法
应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。
19. 什么是缓冲区溢出?有什么危害?其原因是什么?
1. 缓冲区溢出:是指当计算机向缓冲区填充数据时超过了缓冲区的内存大小,溢出的数据覆盖在合法数据上。
2. 危害:缓冲区溢出,最危险的就是堆栈溢出,导致入侵者可以改变函数的返回值,让其跳到任意地址,或者跳转并且执行一段恶意代码,从而破坏程序、系统。
3. 产生原因:造成缓冲区溢出的主原因是程序中没有仔细检查用户输入的参数。
20. 程序编译与链接
build构建过程分为四步:预处理、编译、链接、汇编
1. 预处理
主要处理源文件中以“#”开始的预编译指令,主要处理规则有:
1. 将所有的#define删除,展开所用的宏定义
2. 处理所有的条件编译指令,比如:#if、#ifdef、#elif、#endif
3. 处理#include预编译指令,将被包含的文件递归地插入到该编译指令的位置
4. 保留所有#pragma编译器指令
5. 删除所有的注释
6.添加行号和文件名标志。以便编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时可显示行号
2. 编译
编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相关的汇编代码文件。这是整个程序构建的核心部分。
3. 链接
链接的主要内容是把各个模块之间相互引用的部分处理好,使各个模块可以正确的拼接。
链接的主要过程:地址空间的分配、符号决议、重定向等步骤
其中:
动态链接和静态链接
1)动态链接方法:等到需要真正调用动态库的时候才分配动态代码的逻辑地址,所以初始时间短,但是运行期间的性能比不上静态链接
2)静态链接方法:载入代码就会把程序会用到的动态代码或动态代码的地址确定下来
4. 汇编
将汇编代码转化成机器可以执行的指令,每一条汇编语句几乎都是一条机器指令。经过编译、链接、汇编输出的文件成为目标文件(Object File)
21. 边沿触发和水平触发
边缘触发是指每当状态变化时发生一个 io 事件
条件触发是只要满足条件就发生一个 io 事件
参考:
https://zhuanlan.zhihu.com/p/23755202
https://github.com/taizilongxu/interview_python/blob/master/Readme.md#10-args-and-kwargs