操作系统高频面试问题总结

本文详细总结了操作系统面试中常见的问题,包括进程与线程的区别、进程通信方法、线程同步与互斥策略、内存管理和调度算法等。还探讨了异常、中断、内存分配策略、页面置换算法以及死锁避免等高级主题,对于准备操作系统面试的开发者极具参考价值。

进程与线程的区别和联系

link
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
(4)系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。

一个进程可以创建多少线程,和什么有关

在32位Linux下,理论上,一个进程可用虚拟空间是3G,默认情况下,线程的栈的大小是10MB,所以理论上最多只能创建300多个线程。如果要创建多于300的话,必须修改编译器的设置。
一个进程可以创建的线程数由可用虚拟空间和线程的栈的大小共同决定,只要虚拟空间足够,那么新线程的建立就会成功。如果需要创建超过300以上的线程,减小你线程栈的大小就可以实现了。

一个程序从开始运行到结束的完整过程(四个过程)

1.操作系统在创建进程后,把控制权交到程序的入口,这个入口往往是运行库中的某个入口函数
2.入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造
3.入口函数在完成初始化后,调用main函数,正式开始执行程序主体部分
4.main函数执行完毕后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后系统调用结束进程

预处理:读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。
编译:通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
汇编:实际上指把汇编语言代码翻译成目标机器指令的过程。
链接:主要分为两种:静态链接和动态链接;
静态链接:后缀是.a,主要在编译的时候将库文件里面代码搬迁到课执行的文件中;
动态链接:后缀是.so,主要在执行的时候将需要的库文件代码搬迁到可以执行的文件中;
link

进程通信方法(Linux和windows下),线程通信方法(Linux和windows下)

同步进程通信:管道、FIFO(命名管道)、消息队列、共享内存、信号量(用于进程同步)、socket套接字
异步进程通信:信号

线程互斥和同步的方法
互斥:互斥量、读写锁、自旋锁
同步:轮询结合互斥量、条件变量、信号量、屏障

文件读写使用的系统调用

link

怎么回收线程

pthread_join函数

守护进程、僵尸进程和孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lqd等(这里的结尾字母d就是Daemon的意思)。
link

处理僵尸进程的两种经典方法

1.调用fork两次(第一次调用产生一个子进程,第二次调用fork是在第一个子进程中调用,同时将父进程退出(第一个子进程退出),此时的第二个子进程的父进程id为init进程id(注意:新版本Ubuntu并不是init的进程id))。
2.捕获SIGCHLD信号并在捕获程序中调用wait/waitpid函数。

进程终止的几种方式

正常终止五种:
1.从main返回。
2.调用exit。
3.调用_exit或_Exit。
4.最后一个线程从其启动例程返回。
5.最后一个线程调用pthread_exit。
三种异常终止:
6.调用abort()。
7.接到一个信号并终止。
8.最后一个线程对取消请求作出响应。
link

linux中异常和中断的区别

相同点:
1> 最后都是由CPU发送给内核,由内核去处理
2> 处理程序的流程设计上是相似的
不同点:
1> 产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的
2> 内核需要根据是异常还是中断调用不同的处理程序
3> 中断不是时钟同步的,这意味着中断可能随时到来;异常由于是CPU产生的,所以,它是时钟同步的
4> 当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中
link

一般情况下在Linux/windows平台下栈空间的大小

1)查看linux默认栈空间的大小
  通过命令 ulimit -s 查看linux的默认栈空间大小,默认情况下为8192 KB 即8MB。
(2)临时改变栈空间的大小
  通过命令 ulimit -s 设置大小值临时改变栈空间大小。例如:ulimit -s 102400,即修改为100MB。
(3)永久修改栈空间大大小。有两种方法:
  方法一:可以在/etc/rc.local 内加入 ulimit -s 102400 则可以开机就设置栈空间大小,任何用户启动的时候都会调用。
  方法二:修改配置文件/etc/security/limits.conf

五种IO模型

1.阻塞IO模型
2.非阻塞IO模型
3.IO复用模型
4.信号驱动IO
5.异步IO模型
link

程序从堆中动态分配内存时,虚拟内存上怎么操作的

在Linux下,glibc 的malloc提供了下面两种动态内存管理的方法:堆内存分配和mmap的内存分配,此两种分配方法都是通过相应的Linux 系统调用来进行动态内存管理的。具体使用哪一种方式分配,根据glibc的实现,主要取决于所需分配内存的大小。一般情况中,应用层面的内存从进程堆中分配,当进程堆大小不够时,可以通过系统调用brk来改变堆的大小,但是在以下情况,一般由mmap系统调用来实现应用层面的内存分配:A、应用需要分配大于1M的内存,B、在没有连续的内存空间能满足应用所需大小的内存时。

(1)、调用brk实现进程里堆内存分配

在glibc中,当进程所需要的内存较小时,该内存会从进程的堆中分配,但是堆分配出来的内存空间,系统一般不会回收,只有当进程的堆大小到达最大限额时或者没有足够连续大小的空间来为进程继续分配所需内存时,才会回收不用的堆内存。在这种方式下,glibc会为进程堆维护一些固定大小的内存池以减少内存脆片。

(2)、使用mmap的内存分配

在glibc中,一般在比较大的内存分配时使用mmap系统调用,它以页为单位来分配内存的(在Linux中,一般一页大小定义为4K),这不可避免会带来内存浪费,但是当进程调用free释放所分配的内存时,glibc会立即调用unmmap,把所分配的内存空间释放回系统。

注意:这里我们讨论的都是虚拟内存的分配(即应用层面上的内存分配),主要由glibc来实现,它与内核中实际物理内存的分配是不同的层面,进程所分配到的虚拟内存可能没有对应的物理内存。如果所分配的虚拟内存没有对应的物理内存时,操作系统会利用缺页机制来为进程分配实际的物理内存。

交换空间与虚拟内存的关系

为了提高磁盘存取效率, Linux做了一些精心的设计, 除了对dentry进行缓存(用于VFS,加速文件路径名到inode的转换), 还采取了两种主要Cache方式:Buffer Cache和Page Cache.前者针对磁盘块的读写,后者针对文件inode的读写.这些Cache有效缩短了I/O系统调用(比如 read,write,getdents)的时间.
内存活动基本上可以用3个数字来量化:活动虚拟内存总量,交换(swapping)率和调页(paging)率.其中第一个数字表明内存的总需求量,后两个数字表示那些内存中有多少比例正处在使用之中.目标是减少内存活动或增加内存量,直到调页率保持在一个可以接受的水平上为止.

活动虚拟内存的总量(VM)=实际内存大小(size of real memory)(物理内存)+使用的交换空间大小(amount of swap space used)

当程序运行需要的内存大于物理内存时,Linux系统采用了调页机制,即系统copy一些内存中的页面到磁盘上,腾出来空间供进程使用。

大多数系统可以忍受偶尔的调页,但是频繁的调页会使系统性能急剧下降。

Linux内存管理:Linux系统通过2种方法进行内存管理,“调页算法”,“交换技术”。

调页算法是将内存中最近不常使用的页面换到磁盘上,把常使用的页面(活动页面)保留在内存中供进程使用。

交换技术是系统将整个进程,而不是部分页面,全部换到磁盘上。正常情况下,系统会发生一些交换过程。

当内存严重不足时,系统会频繁使用调页和交换,这增加了磁盘I/O的负载。进一步降低了系统对作业的执行速度,即系统I/O资源问题又会影响到内存资源的分配。

Linux的虚拟内存是一个十分复杂的子系统,它实现了进程间代码与数据共享机制的透明性,并能够分配比系统现有物理内存更多的内存,某些操作系统的虚存甚至能通过提供缓存功能影响到文件系统的性能,各种风格的Linux的虚存的实现方式区别很大,但都离不开下面的4个概念。
  1:实际内存
  实际内存是指一个系统中实际存在的物理内存,称为RAM。实际内存是存储临时数据最快最有效的方式,因此必须尽可能地分配给应用程序,现在的RAM的形式有多种:SIMM、DIMM、Rambus、DDR等,很多RAM都可以使用纠错机制(ECC)。
  2:交换空间
  交换空间是专门用于临时存储内存的一块磁盘空间,通常在页面调度和交换进程数据时使用,通常推荐交换空间的大小应该是物理内存的二到四倍。
  3:页面调度
  页面调度是指从磁盘向内存传输数据,以及相反的过程,这个过程之所以被称为页面调度,是因为Linux内存被平均划分成大小相等的页面;通常页面大小为 4KB和8KB(在Solaris中可以用pagesize命令查看)。当可执行程序开始运行时,它的映象会一页一页地从磁盘中换入,与此类似,当某些内存在一段时间内空闲,就可以把它们换出到交换空间中,这样就可以把空闲的RAM交给其他需要它的程序使用。
  4:交换
  页面调度通常容易和交换的概念混淆,页面调度是指把一个进程所占内存的空闲部分传输到磁盘上,而交换是指当系统中实际的内存已不够满足新的分配需求时,把整个进程传输到磁盘上,交换活动通常意味着内存不足。
  vmstat监视内存性能:该命令用来检查虚拟内存的统计信息,并可显示有关进程状态、空闲和交换空间、调页、磁盘空间、CPU负载和交换,cache刷新以及中断等方面的信息。

堆和栈的区别;从堆和栈上建立对象哪个快?(考察堆和栈的分配效率比较)

link
在具体的C/C++编程框架中,这两个概念并不是并行的。栈是机器系统提供的数据结构,而堆栈是C/C++函数库提供的。
具体来说,现代计算机,都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统支持的数据类型,并不直接支持其他的数据结构。

和栈不同,堆的数据结构并不是由系统支持的,而是有函数库提供的。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理,比如和其他空间合并成更大的空闲空间。
使用这么复杂的机制有如下原因:

系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类会造成浪费。
系统调用申请内存代价昂贵,涉及用户态和核心态的转换
没有管理的内存分配在大量复杂内存的分配释放下很容易造成内存碎片。
从这里我们可以看到,堆和栈相比,由于大量 new/delete 的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。

那么?我们应该荐尽量用栈,而不用堆吗?并不是的。
虽然栈有很多的好处,但是由于和堆相比不是那么灵活,且分配大量的内存空间,堆更加适合。

内存泄漏和内存溢出

1、内存溢出
内存溢出是指程序在申请内存时没有足够的内存空间供其使用。原因可能如下:
(1)内存中加载的数据过于庞大;
(2)代码中存在死循环;
(3)递归调用太深,导致堆栈溢出等;
(4)内存泄漏最终导致内存溢出;
2、内存泄漏
内存泄漏是指使用new申请内存, 但是使用完后没有使用delete释放内存,导致占用了有效内存。

常见内存分配方式和错误

link
1.内存分配未成功,却使用了它。
2.内存分配虽然成功,但是尚未初始化就引用它。
3.内存分配成功并且已经初始化,但操作越过了内存的边界。
4.忘记了释放内存,造成内存泄露。
5.释放了内存却继续使用它。
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。

【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。

【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。

堆内存和栈内存的区别

管理方式:对于栈来讲,是由编译器自动管理,无需手动控制;对于堆来说,分配和释放都是由程序员控制的。

空间大小:总体来说,栈的空间是要小于堆的。一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的;但是对于栈来讲,一般是有一定的空间大小的。

碎片问题:对于堆来讲,由于分配和释放是由程序眼控制的(利用new/delete 或 malloc/free),频繁的操作势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的数据结构,在某一对象弹出之前,它之前的所有对象都已经弹出。

生长方向:对于堆来讲,生长方向是向上的,也就是沿着内存地址增加的方向,对于栈来讲,它的生长方式是向下的,也就是沿着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配,静态分配是编译器完成的,比如局部变量的分配;动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器实现的,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率很高。堆则是C/C++函数提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率要比栈底的多。

可重入函数和可重入内核

link
可重入内核在ULK(深入理解linux内核)中的定义是指若干个进程可以同时在内核态下执行,也就是说多个进程可以在内核态下并发执行内核代码。这里的可重入,是指可以多个进程进入内核,并不是重复/重新进入内核。
对于linux来说,可重入内核代码包含可重入函数和非可重入函数。

可重入函数的理解其实比较麻烦,可以从以下阐述:
1.可重入是与多线程无关的,一个函数被同一个线程调用2次以上,得到的结果具有可再现性。则这个函数是可重入的。
2.可重入讲究的是结果可再现性,因此,使用全局(静态)变量的函数,再次调用其函数的结果是不可再现的,这就是前面说的为何要求该函数只修改局部变量
故可重入函数,描述的是函数被多次调用但是结果具有可再现性
可重入函数条件:
1,不在函数内部使用静态或者全局数据
2,不返回静态或者全局数据,所有的数据都由函数调用者提供
3,使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4, 如果必须访问全局数据,使用互斥锁(自旋锁)来保护
5,不调用不可重入函数
6,可重入函数必须是线程安全的

操作系统动态内存分配的几种策略

link
第一种:单一连续分配方式

适用于单用户、单任务的操作系统。没什么好讲的。

第二种:固定分区分配

此种分配方式把内存空间分为固定大小的区域,每个分区允许一个作业被装入。分区大小可以不相同。通常会建立一张分区使用表来记录每个分区的起始地址、分区大小、状态。没有足够大的分区则拒绝分配内存。此种分配方式是最早的多道程序的存储管理方式。

缺点:限制了进程的数目,内存空间利用率比较低。

第三种:动态分区分配

此种方式涉及到相应的数据结构(分区表、分区链),分区分配算法和回收操作。

分区分配算法有:首次适应算法 ( 以链表结构为例,下同。从链首开始顺序查找,找到一个符合条件的分区即可进行相应的分配,没有符合条件的则分配失败 ) 、循环首次适应算法(从上一次符合条件的分区进行循环查找 ) 、最佳适应算法(首先需要把空闲分区链表按容量排序 [ 排序的目的是为了加速查找,否则就要遍历整个链表 ] ,然后从链首进行顺序查找 ) 、最坏适应算法( 选择最大的空闲分区,然后进行分配 ) 、快速适应算法 ( 分类搜索算法,采取分区表加上相同类别管理的链表进行记录,仅需根据进程的长度,即可分配相应的内存空间 )。

回收内存的方式:只要回收空间与空闲分区相邻接,那么仅需与空闲分区合并即可;否则,需为回收区单独建立一项新的表,然后把回收区的首地址插入到空闲链中相应的位置。

缺点:相应分配的算法比较复杂,回收空间需要合并分区,系统开销大。

第四种:伙伴系统

规定:已分配区间或空闲区间的大小均为2的k次幂。

具体:当进程需要一个长度为n的空间时,需要计算一个i值,使得2的i-1次方小于n,2的i次方大于等于n。然后根据计算结果,得到空闲分区链表中查找大小为2的i次方的空闲分区,如果不存在这样的分区,则将2的i+1次方化成两个2的i次方的空闲分区,以此类推,总有符合的空闲分区。回收与分配空间的方式恰好相反。

第五种:哈希算法

在分类搜索算法的基础上,利用哈希快速查找的优点,快速到查找相同容量类别的链表,实现最佳的分配策略。

第六种:可重定位分区分配

此种算法考虑到的情况是:有很多内存碎片。对于一个进程来说,没有任何一个碎片能够满足进程所需的容量要求,但是碎片的容量总和能够满足一个或者多个进程的容量要求。

解决方案:①把内存中的所有作业全部移动,让他们紧凑在一起,这样内存碎片便集中在一起了。(需要对移动的程序地址进行修改才行)

分区分配算法:与动态分区分配算法类似,不过多了“紧凑”的操作。

第七种:对换

将占用内存却没有干什么事情的进程给放到对换区(外存分为文件区和对换区)。

内部碎片和外部碎片

内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。

外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。

系统调用进入内核态的过程

link
在这里插入图片描述

内核态和用户态的区别

link

常见的进程调度算法以及linux的进程调度

link

中断、陷阱、故障和终止

link

进程通信方法

同步进程通信:管道、FIFO(命名管道)、消息队列、共享内存、信号量(用于进程同步)、socket套接字
异步进程通信:信号

线程互斥和同步的方法

互斥:互斥量、读写锁、自旋锁
同步:轮询结合互斥量、条件变量、信号量、屏障

内存对齐的规则和作用

link

页面置换算法

最佳
最近最久未使用(LRU)
最近未使用(NRU)
先进先出(FIFO)
第二次机会算法
时钟算法

实现一个LRU页置换算法(或者FIFO置换算法)

死锁的必要条件(怎么检测死锁,解决死锁问题),银行家算法(死锁避免)

link

哲学家就餐,读者写者,生产者消费者(怎么加锁解锁,伪代码)

link

海量数据的bitmap使用原理

link

布隆过滤器原理与优点

link

布隆过滤器处理大规模问题时的持久化,包括内存大小受限、磁盘换入换出问题

link

共享内存实现

link

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值