C/C++后端开发面经(1)——计算机操作系统
C/C++后端开发面经(1)——计算机操作系统
- 1.1 进程线程的基本概念
- 1.1.1 什么是进程,线程?
- 1.1.2 多进程、多线程的优缺点
- 1.1.3 什么时候用进程,什么时候用线程
- 1.1.4 单线程和多线程区别
- 1.1.5 多进程、多线程同步(通讯)的方法
- 1.1.5.1 进程间通讯
- 1.1.5.2 线程通讯
- 1.1.6互斥锁与信号量的区别?
- 1.1.7 进程的空间模型
- 1.1.8 静态变量和全局变量的区别
- 1.1.9 一个进程可以创建多少线程,和什么有关
- 1.1.10 进程线程的状态转换图,什么时候阻塞,什么时候就绪?
- 1.1.11 父进程、子进程的关系以及区别
- 1.1.12 进程创建子进程,fork详解
- 1.1.13 什么是进程上下文、中断上下文
- 1.1.14 那么运行在中断上下文的代码受到限时,有什么不能做的事呢?
- 1.1.15 死锁的原因、条件 创建一个死锁,以及如何预防⭐⭐⭐⭐⭐
- 1.2 孤儿进程、僵尸进程、守护进程的概念
- 1.2.1 基本概念
- 1.2.2 如何创建守护进程:
- 1.2.2 正确处理孤儿进程、僵尸进程的方法
- 1.3 并发,同步,异步,互斥,阻塞,非阻塞
- 1.3.1 概念
- 1.3.1.1并发:
- 1.3.1.2 同步、互斥:
- 1.3.1.3 同步、异步:
- 1.3.1.4 阻塞,非阻塞:
- 1.3.1.5 同步阻塞,同步非阻塞,异步阻塞,异步非阻塞:
- 1.3.2 什么是线程同步和互斥
- 1.3.3 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
- 1.3.4 常见的IO模型,五种
- 1.3.4.1 阻塞I/O
- 1.3.4.2 非阻塞I/O
- 1.3.4.3 I/O复用
- 1.3.4.4 信号驱动I/O
- 1.3.4.5 异步I/O
- 1.3.5 IO复用
- 1.3.5.1 Select
- 1.3.5.2 Poll
- 1.3.5.3 Epoll
- 1.3.5.4 IO复用区别总结:
- 1.3.5.5 epoll 的 LT 和 ET 模式的理解:
- 1.4 内存管理
- 1.4.1 什么是堆,栈,内存泄漏和内存溢出?⭐⭐⭐⭐
- 1.4.2 操作系统由逻辑地址求物理地址
- 1.4.3 虚拟内存,虚拟地址与物理地址的转换⭐⭐⭐⭐
- 1.4.4 扇区 块 页 簇的概念 ⭐⭐
- 1.4.4.1 扇区
- 1.4.4.2 块 簇
- 1.4.4.3 页
- 1.4.5 内零头,外零头,段页式管理⭐⭐⭐⭐
- 1.4.5.1 内零头
- 1.4.5.2 外零头
- 1.4.5.3 段式存储管理
- 1.5 文件管理系统
- 1.5.1 硬链接与软链接的区别;⭐⭐⭐⭐⭐
- 1.5.1.1 首先什么是链接?
- 1.5.1.2. 硬链接
- 1.5.1.3. 软链接(符号链接)
- 1.5.1.4. 硬链接与软链接的区别;
- 1.5.1.5 总结:
- 1.6 中断
- 1.6.1 中断和异常的区别⭐⭐⭐⭐⭐
- 1.6.2 中断怎么发生,中断处理大概流程⭐⭐⭐⭐
- 1.7 其他操作系统常见面试题
- 1.7.1 大小端的区别以及各自的优点,哪种时候用,验证⭐⭐⭐⭐⭐
- 1.7.2 一个程序从开始运行到结束的完整过程(四个过程)⭐⭐⭐⭐
- 1.7.3 堆和栈的区别⭐⭐⭐⭐⭐
- 1.7.4 计算机中,32bit与64bit有什么区别⭐⭐⭐
1.1 进程线程的基本概念
1.1.1 什么是进程,线程?
进程是资源(CPU、内存等)分配的基本单位,线程是CPU调度和分配的基本单位(程序执行的最小单位)。
- 当我们运行一个程序的时候,系统就会创建一个进程,并分配地址空间和其他资源,最后把进程加入就绪队列直到分配到CPU时间就可以正式运行了。
- 线程是进程的一个执行流,有一个初学者可能误解的概念,进程就像一个容器一样,包括程序运行的程序段、数据段等信息,但是进程其实是不能用来运行代码的,真正运行代码的是进程里的线程。
- 那么,来看看我们最熟悉的main()函数,我们既可以认为这是一个进程,也可以认为是一个线程。我们都知道,在C/C++中main函数是程序入口,所以准确来说main函数是程序的主线程。然而很神奇的地方在于,当系统在执行main函数的时候,main函数又是一个独立的进程,我们可以在main函数里创建子进程,也可以创建子线程。
- 在main函数里创建的多个子线程中,每个线程有自己的堆栈和局部变量,但多个线程也可共享同个进程下的所有共享资源,因此我们经常可以创建多个线程实现并发操作,实现更加复杂的功能。
示例:我们看一个实际例子来加强理解。
int g_cnt = 0; //全局变量
int * thread(void * arg)
{int m_cnt = 0;m_cnt = 5;g_cnt++;return 0;
}
int main(void)
{int err = 0;pthread_t tid;int m_cnt = 0;err=pthread_create(&tid, NULL, thread, NULL); //创建子线程if (0 != err) //检验是否创建成功{printf("can't creat thread: %s\n", strerror(err));}while(g_cnt == 0){usleep(300); //延迟300毫秒,让子线程运行一会儿}printf("g_cnt = %d, m_cnt = %d\n", g_cnt, m_cnt);return 0;
}
我们可以看出main函数是一个主线程,开始执行程序,同时main函数又是一个进程,我们可以创建子线程thread(),子线程有自己的堆栈和局部变量,同时又与主线程共享全局变量,这也就是为何输出结果显示子线程改变了全局变量g_cnt,但没有改变main函数里的同名局部变量m_cnt。
还有一个关键点需要注意:我们可以看到main函数有一个while循环,一开始g_cnt等于里,程序进入while循环后就不能做其他事情,但是子线程thread不受影响,仍然可以独立于main函数,自己做自己的事情。
1.1.2 多进程、多线程的优缺点
解析:为了理解多进程、多线程各自的优缺点之前,我们需要先了解进程和线程最大的区别和联系,一个进程由PCB(进程控制块)、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,跟人多好干活的道理一样,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。
- 多进程更健壮,一个进程死了不影响其他进程,子进程死了也不会影响到主进程,毕竟系统会给每个进程分配独立的系统资源。多线程比较脆弱,一个线程崩溃很可能影响到整个程序,因为多个线程是在一个进程里一起合作干活的。
- 进程性能大于线程,每个进程独立地址空间和资源,而多个线程是一起共享了同个进程里的空间和资源,结果就很明显了,线程的性能上限一定比不上进程。
- 正因为进程性能大于线程。所以这也引发了另一重要知识点,创建多进程的系统花销远大于创建多线程。
- 多进程通讯因为需要跨越进程边界,不适合大量数据的传送,更适合小数据或者密集数据的传送。而多线程无需跨越进程边界,适合各线程间大量数据的传送,甚至还有很重要的一点,多线程可以共享同一进程里的共享内存和变量哦。
- 多进程逻辑控制比多线程复杂,需要与主进程做好交互。根据上面几点,我们不难知道多进程是“要用来做大事”的,而多线程是“各自做件小事,合作完成大事”。所以要做大事自然就需要更复杂的逻辑控制,不像做小事那么目标明显。
- 虽然多线程逻辑控制比较简单,但是却需要复杂的线程同步和加锁控制等机制,而进程就不需要了。
- 最后的一点,可能比较少见,我们可以通过增加CPU的数量来增加进程的数量,但增加不了线程的数量,即增加CPU无法提高线程数量,线程数量由进程的空间资源和线程本身栈大小确定,详情见1.1.6小节。
1.1.3 什么时候用进程,什么时候用线程
解析:还是同一个思想,进程是“要用来做大事”的,而线程是“各自做件小事,合作完成大事”,解析结合上节新鲜出炉的优缺点我们就很好理解什么时候用进程或者线程了。
- 创建和销毁较频繁使用线程,因为创建进程花销大嘛。
- 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
- 并行操作使用线程。线程是为了实现并行操作的一个手段,也就是刚才说的需要多个并行操作“合作完成大事”,当然是使用线程啦。
- 最后可以总结为:安全稳定选进程;快速频繁选线程;
1.1.4 单线程和多线程区别
- 多线程好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。 - 多线程的不利方面:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug;
1.1.5 多进程、多线程同步(通讯)的方法
当我们在使用系统编程时,就会遇到多进程、多线程编程,所以必须知道在多个进程、多个线程之间都有什么常见的通讯机制,这也是嵌入式面试中高频问题之一。
1.1.5.1 进程间通讯
进程间通讯:
(1)管道/无名管道(2)信号(3)共享内存(4)消息队列(5)信号量(6)socket
注意:临界区则是一种概念,指的是访问公共资源的程序片段,并不是一种通信方式。
- 管道,通常指无名管道。
- 半双工,具有固定的读端和写端;
- 只能用于具有亲属关系的进程之间的通信;
- 可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数。但是它不是普通的文件,并不属于其他任何文件系统,只能用于内存中。
4)Int pipe(int fd[2]); 当一个管道建立时,会创建两个文件文件描述符,要关闭管道只需将这两个文件描述符关闭即可。
- FiFO(有名管道)
- FIFO可以再无关的进程之间交换数据,与无名管道不同;
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中;
- Int mkfifo(const char pathname,mode_t mode);*
- 消息队列
- 消息队列,是消息的连接表,存放在内核中。一个消息队列由一个标识符来标识;
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询
- 信号量
- 信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
- 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作;
- 共享内存
- 共享内存,指两个或多个进程共享一个给定的存储区;
- 共享内存是最快的一种进程通信方式,因为进程是直接对内存进行存取;
- 因为多个进程可以同时操作,所以需要进行同步;
- 信号量+共享内存通常结合在一起使用。
1.1.5.2 线程通讯
线程通讯(同步):
(1)信号量(2)读写锁(3)条件变量(4)互斥锁(5)自旋锁
多线程通过特定的设置来控制线程之间的执行顺序,也可以说在线程之间通过同步建立起执行顺序的关系;
主要四种方式:临界区、互斥对象、信号量、事件对象;其中临界区和互斥对象主要用于互斥控制,信号量和事件对象主要用于同步控制;
- 临界区:
通过对多线程的串行化来访问公共资源或一段代码,速度快、适合控制数据访问。在任意一个时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。 - 互斥对象:
互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。 - 信号量:
它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。但是当前可用计数减小 到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离 开的同时通过ReleaseSemaphore ()函数将当前可用资源计数加1 。在任何时候当前可用资源计数决不可能大于最大资源计数。 - 事件对象:
通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
1.1.6互斥锁与信号量的区别?
互斥锁用于线程的互斥,信号量用于线程的同步。这是互斥锁和信号量的根本区别,也就是互斥和同步之间的区别。同时互斥锁的作用域仅仅在于线程,信号量可以作用于线程和进程。
1.1.7 进程的空间模型
解析:32位系统中,当系统运行一个程序,就会创建一个进程,系统为其分配4G的虚拟地址空间,其中0-3G是用户空间,3-4G是内核空间,具体如图所示,内核空间是受保护的,用户不能对该空间进行读写操作,否则可能出现段错误。其中栈空间有向下的箭头,代表数据地址增加的空间是往下的,新的数据的地址的值反而更小,堆空间则是往上。
- 栈区:由编译器自动分配和释放,存放函数的参数值(形参)、局部变量(int a =1;还有指针变量)等,其操作方式类似于数据结构中的栈,先进后出。
- 堆区:一般由程序员分配和释放,若程序员不释放,可能会造成内存泄漏,程序结束的时候可能由操作系统回收,注意它与数据结构中的堆是两回事,分配方式类似于链表。
- 全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域 (.data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域 (.bss),程序结束后系统释放。
- 文字常量区:常量字符串放在这里,程序结束后有系统释放。
- 程序代码区(.text):存放函数体的二进制代码。
栈的空间有限,堆是很大的自由存储区,程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也是在栈上进行。
注意:64位操作系统下的虚拟内存空间大小:地址空间大小不是232 , 也不是264,而一般是248。因为并不需要264那么大的寻址空间,过大的空间只会造成资源的浪费。所以64位Linux一般使用48位表示虚拟空间地址,40位标识物理地址。
0x0000000000000000 ~ 0x00007fffffffffff表示用户空间,
0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF表示内核空间,共提供256TB(248)的寻址空间。
1.1.8 静态变量和全局变量的区别
全局变量与全局静态变量的区别:
(1)若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
(2)若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享, 即:全局静态变量对组成该程序的其它源文件是无效的。
静态全局变量的作用:
(1)不必担心其它源文件使用相同变量名,彼此相互独立。
(2)在某源文件中定义的静态全局变量不能被其他源文件使用或修改。
1.1.9 一个进程可以创建多少线程,和什么有关
一个进程创建线程的个数由虚拟内存和分配给线程的调用栈大小决定。
由1.1.5小节我们已经知道创建一个进程,系统会分配4G的虚拟内存,其中1G是内核空间,只有3G是用户空间,也就是我们可以利用来创建线程的空间大小,一个线程的栈的大小可以通过ulimit -s指令来查看,一般大多是8M-10M。
举个例子,这里不放假设我们创建一个线程的栈需要占用10M内存,因此在3G的空间大概可以创建300个线程。此时如果将线程栈大小增加到20M,那么个数就将减少。
1.1.10 进程线程的状态转换图,什么时候阻塞,什么时候就绪?
解析:在此之前,我们先看看一个进程一生中,从蛋生到死亡都有可能出现什么状态。
- 创建态(New): 一个进程正在被创建,还没到转到就绪状态之前的状态。
- 就绪态(Ready): 一个进程获得了除CPU时间片之外的一切所需资源,一旦得到CPU时间片调度时即可运行。
- 运行/执行态(Running):当一个进程得到CPU调度正在处理机上运行时的状态。
- 睡眠/挂起态:由于某些资源暂时不可得到而进入“睡眠态”,将进程挂起,等待唤醒。
- 阻塞/暂停态(Blocked):一个进程正在等待某一事件而暂停运行时,如等待某资源成为可用,或等待文件读取完成等。
- 结束/僵尸态(Exit):一个进程正在从系统中消失时的状态,这是因为进程结束或其它因流产所导致。
- 死亡态:进程生命周期结束了,将所占用的资源还给系统。
我们从父进程调用fork()创建子进程开始讲起,此时子进程处于创建态,此时系统为进程分配地址和资源后将进程加入就绪队列,进入就绪态。就绪态的进程得到CPU时间片调度正式运行,进入执行态。执行态有四种常见结果:
- 当时间片耗光或者被其他进程抢占,则重新进入就绪态,等待下一次CPU时间片;
- 由于某些资源暂时不可得到而进入“睡眠态”(如欲读取的文件为空或者欲获得的某个锁还处于不可获得状态),等待资源可得后再唤醒,唤醒后进入就绪态;
- 收到SIGSTOP/SIGTSTP信号进入暂停态,直到收到SIGCONT信号重新进入就绪态;
- 进程执行结束,通过内核调用do_exit()进入僵尸态,等待系统回收资源。当父进程调用wait()/waitpid()后接收结束子进程,该进程进入死亡态。
1.1.11 父进程、子进程的关系以及区别
解析:我们先来看看子进程会从父进程继承了什么,以及子进程独有的数据:
- 子进程继承父进程:
○用户号UIDs和用户组号GIDs
○环境Environment
○堆栈
○共享内存
○打开文件的描述符
○执行时关闭(Close-on-exec)标志
○信号(Signal)控制设定
○进程组号
○当前工作目录
○根目录
○文件方式创建屏蔽字
○资源限制
○控制终端 - 子进程独有的:
○进程号PID
○不同的父进程号
○自己的文件描述符和目录流的拷贝
○子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
○不继承异步输入和输出
父进程调用fork()以后,克隆出一个子进程,子进程和父进程拥有相同内容的代码段、数据段和用户堆栈。但其实父进程只复制了自己的PCB块,而代码段,数据段和用户堆栈内存空间是与子进程共享的。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。
在面试前,我们需要记清楚、分清楚几个主要的父子进程共有的资源和子进程独有的资源。
子进程从父进程继承的主要有:用户号和用户组号;堆栈;共享内存;目录(当前目录、根目录);打开文件的描述符;但父进程和子进程拥有独立的地址空间和PID参数、不同的父进程号、自己的文件描述符。
1.1.12 进程创建子进程,fork详解
- 函数原型
pid_t fork(void); //void代表没有任何形式参数 - 0
除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进程,即调用fork函数的进程为父进程,新建的进程为子进程。 - fork函数不需要任何参数,对于返回值有三种情况:
①对于父进程,fork函数返回新建子进程的pid;
②对于子进程,fork函数返回 0;
③如果出错, fork 函数返回 -1。
int pid=fork();
if(pid < 0){//失败,一般是该用户的进程数达到限制或者内存被用光了
........
}
else if(pid == 0){//子进程执行的代码
......
}
else{//父进程执行的代码
.........
}
1.1.13 什么是进程上下文、中断上下文
由1.1.5知道进程空间分为内核空间和用户空间,即内核功能模块运行在内核空间,而我们编写的应用程序运行在用户空间。其中内核运行在最高权限级别的内核态,这个级别有最高权限可以进行所有操作,而应用程序运行在较低级别的用户态,内核态和用户态有自己的内存映射,即自己的地址空间。
- 进程上下文:
正是有了不同运行状态的划分,才有了上下文的概念。当我们创建一个进程(例如main函数)需要控制一个外部设备时(比如控制一个LED灯亮),我们编写的在用户空间的代码将通过“系统调用(操作系统提供给用户空间的接口函数)”进入内核空间,由内核继续代表我们这个进程运行于内核空间,这时候就涉及上下文的切换。用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。 - 中断上下文:
同理,当由硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。
1.1.14 那么运行在中断上下文的代码受到限时,有什么不能做的事呢?
- 睡眠或者放弃CPU。
这样做的后果是灾难性的,因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉 - 尝试获得信号量
如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况 - 执行耗时的任务
中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。 - 访问用户空间的虚拟地址
因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在终端上下文无法访问用户空间的虚拟地址。
1.1.15 死锁的原因、条件 创建一个死锁,以及如何预防⭐⭐⭐⭐⭐
面试中常问死锁的原因和必要条件,务必记清楚。死锁预防就当作提升。
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁死锁的四个必要条件
只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。死锁预防:
我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
(1)破坏“不可剥夺”条件:
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
(2)破坏”请求与保持条件“:
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
(3) 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
1.2 孤儿进程、僵尸进程、守护进程的概念
1.2.1 基本概念
- 孤儿进程:当父进程退出后它的子进程还在运行,那么这些子进程就是孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
- 僵尸进程:当子进程退出后而父进程并未接收结束子进程(如调用waitpid获取子进程的状态信息),那么子进程仍停留在系统中,这就是僵尸进程。
- 守护进程:是在后台运行不受终端控制的进程(如输入、输出等)。网络服务大部分就是守护进程。
1.2.2 如何创建守护进程:
- 创建子进程,父进程退出:
因为守护进程是在后台运行不受终端控制的进程,父进程退出后控制台就以为该程序结束了,我们就可以在子进程进行自己的任务,同时用户仍可以在控制台输入指令,从而在形式上做到了与控制台脱离。 - 在子进程中创建新的会话(脱离控制终端):
使用系统函数setsid()来创建一个新的会话,并担任该会话组的组长,摆脱原会话的控制==>摆脱原进程的控制==>摆脱原控制台的控制。 - 改变当前目录为根目录:
1.1.7小节知道子进程继承父进程的目录信息,但进程运行时对当前目录下的文件系统不能卸载,这会有很多隐藏的麻烦,建议使用根目录作为当前目录,当然也可以使用其他目录。 - 重设文件权限掩码,关闭文件描述符:
子进程还继承父进程文件权限掩码,即屏蔽掉文件权限中的对应位。此时子进程需将其重置为0,即在此时有大的权限,从而提高该守护进程灵活度。最后,关系从父进程继承的已经打开的文件描述符,如不进行关闭将造成浪费资源以及子进程所有文件系统无法卸载等错误。
代码如下:
int main(int argc, const char *argv[])
{pid_t pid;pid = fork();If(pid < 0) //创建子进程失败{perror("fail to fork");exit(0);}else if(pid > 0){ //父进程退出exit(0);}else{ //进入子进程setsid(); //创建新会话umask(0); //重置文件权限掩码pid = fork();if(pid != 0){exit(0);}chdir("/"); //设置当前目录为根目录int maxfd = getdtablesize();while(maxfd--){close(maxfd); //关闭文件描述符}while(1){syslog(LOG_INFO,"im deamon\n");sleep(1);}}return 0;
}
可以发现子进程里再次创建了一个子进程,虽非必要,但却是对守护进程进行一点优化:
第一次fork:这里第一次fork的作用在shell终端里造成一个程序已经运行完毕的假象,同时创建新会话的进程不能是进程组组长,所以父进程是进程组组长是不能创建新会话的,需要子进程中执行。所以到这里子进程便成为了一个新会话组的组长啦。
第二次fork:第二次fork可以保证不会因为错误操作重新打开终端,因为只有会话组组长可以打开一个终端,再第二次fork后的子进程就不是会话组组长啦。
1.2.2 正确处理孤儿进程、僵尸进程的方法
- 孤儿进程的处理:
孤儿进程也就是没有父进程的进程,孤儿进程的处理就由进程号为1的Init进程负责,就像一个福利院一样,专门负责处理孤儿。当有孤儿进程需要处理的时候,系统就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。因此孤儿进程并不会有什么危害。 - 僵尸进程的处理:
如果父进程一直不调用wait/waitpid函数接收子进程,那么子进程就一直保存在系统里,占用系统资源,因此如果僵尸进程数量太多,那么就会导致系统空间爆满,无法创建新的进程,严重系统工作,因此僵尸进程需要好好处理。 - 正确的处理方式:
系统规定,子进程退出后,父进程会自动收到SIGCHLD信号。因此我们需要在父进程里重置signal函数。每当子进程退出,父进程都会收到SIGCHLD信号,故通过signal函数,重置信号响应函数。代码和注释如下:
void* handler(int sig)
{int status;if(waitpid(-1, &status, WNOHANG) >= 0){printf("child is die\n");}
}
int main()
{signal(SIGCHLD, handler);int pid = fork();if(pid > 0) //父进程循环等待{while(1){sleep(2);}}else if(0 == pid){ //子进程说自己die后就结束生命周期,之后父进程就收到SIGCHLD//信号调用handler函数接收结束子进程,打印child is die。printf("i am child, i die\n");}
}
##注意##:handler函数里不能使用wait()函数,比如同一时间有5个子进程都要结束了,向父进程发送SIGCHLD信号,但父进程此时就在处理其中一个,在处理结束前,收到的其他SIGCHLD信号会忽略,导致漏掉部分子进程没有处理结束。
1.3 并发,同步,异步,互斥,阻塞,非阻塞
1.3.1 概念
1.3.1.1并发:
在操作系统中,同个处理机上有多个程序同时运行即并发。并发可分为同步和互斥。
1.3.1.2 同步、互斥:
- 互斥:
分布在不同进程之间的若干程序片断,规定当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如有同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源,但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 - 同步:
分布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。所以同步就是在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
总结:同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
1.3.1.3 同步、异步:
- 同步:
同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。 - 异步:
异步和同步是相对的,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。
注意:
(1)线程是实现异步的一个方式。可以在主线程创建一个新线程来做某件事,此时主线程不需等待子线程做完而是可以做其他事情。
(2)异步和多线程并不是一个同等关系。异步是最终目的,多线程只是我们实现异步的一种手段。
1.3.1.4 阻塞,非阻塞:
阻塞和非阻塞是当进程在访问数据时,根据IO操作的就绪状态不同而采取的不同处理方式,比如主程序调用一个函数要读取一个文件的内容,阻塞方式下主程序会等到函数读取完再继续往下执行,非阻塞方式下,读取函数会立刻返回一个状态值给主程序, 主程序不等待文件读取完就继续往下执行。一般来说可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞。
1.3.1.5 同步阻塞,同步非阻塞,异步阻塞,异步非阻塞:
以发送方发出请求要接收方读取某文件内容为例。
- 同步阻塞:
发送方发出请求后一直等待(同步),接收方开始读取文件,如果不能马上得到读取结果就一直等,直到获取读取结果再响应发送发,等待期间不可做其他操作**(阻塞)**。 - 同步非阻塞:
发送方发出请求后一直等待(同步),接收方开始读取文件,如果不能马上的得到读取结果,就立即返回,接收方继续去做其他事情。此时并未响应发送发,发送方一直在等待。直到IO操作(这里是读取文件)完成后,接收方获得读取结果响应发送方,接收方才可以进入下一次请求过程。(实际不应用) - 异步阻塞:
发送方发出请求后,不等待响应,继续其他工作(异步),接收方读取文件如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作(阻塞)。(实际不应用) - 异步非阻塞:
发送方发出请求后,不等待响应,继续其他工作(异步),接收方读取文件如果不能马上得到结果,也不等待,而是马上返回取做其他事情。当IO操作(读取文件)完成以后,将完成状态和结果通知接收方,接收方在响应发送方。(效率最高)
总结:
1)同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。
2)阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。
3)阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。
1.3.2 什么是线程同步和互斥
- 线程同步:
每个线程之间按预定的先后次序进行运行,协同、协助、互相配合。可以理解成“你说完,我再做”。有了线程同步,每个线程才不是自己做自己的事情,而是协同完成某件大事。 - 线程互斥:
当有若干个线程访问同一块资源时,规定同一时间只有一个线程可以得到访问权, 其它线程需要等占用资源者释放该资源才可以申请访问。线程互斥可以看成是一种特殊的线程同步。
1.3.3 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
同步是个过程,阻塞是线程的一种状态:当多个线程访问同一资源时,规定同一时间只有一个线程可以进行访问,所以后访问的线程将阻塞,等待前访问的线程访问完。
注意:线程同步不一定发生阻塞!线程同步的时候,需要协调推进速度,只有当访问同一资源出现互相等待和互相唤醒会发生阻塞。
1.3.4 常见的IO模型,五种
1.3.4.1 阻塞I/O
应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
1.3.4.2 非阻塞I/O
我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
1.3.4.3 I/O复用
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这三个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
1.3.4.4 信号驱动I/O
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
1.3.4.5 异步I/O
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。
1.3.5 IO复用
详解链接
IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进行处理,从而不会在单个IO上阻塞了。Linux中,提供了select、poll、epoll三种接口函数来实现IO复用。
1.3.5.1 Select
- select的缺点:
①单个进程能够监视的文件描述符的数量存在最大限制,通常是1024。由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
②内核/用户空间内存拷贝问题,select需要大量句柄数据结构,产生巨大开销;
③Select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生事件;
④Select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么每次select调用还会将这些文件描述符通知进程。
1.3.5.2 Poll
与select相比,poll使用链表保存文件描述符,没有了监视文件数量的限制,但其他三个缺点依然存在
1.3.5.3 Epoll
上面所说的select缺点在epoll上不复存在,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。Epoll是事件触发的,不是轮询查询的。没有最大的并发连接限制,内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递。
1.3.5.4 IO复用区别总结:
- 支持一个进程所能打开的最大连接数
①Select最大1024个连接,最大连接数有FD_SETSIZE宏定义,其大小是32位整数表示,可以改变宏定义进行修改,可以重新编译内核,性能可能会影响;
②Poll没有最大连接限制,原因是它是基于链表来存储的;
③epoll连接数限数有上限,但是很大; - FD剧增后带来的IO效率问题
①因为每次进行线性遍历,所以随着FD的增加会造成遍历速度下降,效率降低;
②Poll同上;
③因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的现象下降的性能问题。 - 消息传递方式
①Select内核需要将消息传递到用户空间,都需要内核拷贝;
②Poll同上;
③Epoll通过内核和用户空间共享来实现的。
1.3.5.5 epoll 的 LT 和 ET 模式的理解:
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger),LT是默认模式。
区别:
- LT模式:
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。 - ET模式:
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
1.4 内存管理
1.4.1 什么是堆,栈,内存泄漏和内存溢出?⭐⭐⭐⭐
什么是堆,栈,内存泄漏和内存溢出?
解释:内存溢出、内存泄露、内存越界、缓冲区溢出、栈溢出述
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。应用程序一般使用malloc,calloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
内存泄漏(一般指堆内存的泄漏)可以分为4 类:
1.常发性内存泄漏
2.偶发性内存泄漏
3.一次性内存泄漏
4.隐式内存泄漏
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到
- 内存泄漏
是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
一个盘子用尽各种方法只能装4 个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。 - 内存溢出
就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。 - 内存越界:
向系统申请了一块内存,而在使用内存时,超出了申请的范围(常见的有使用特定大小数组时发生内存越界)
注意:内存越界跟内存溢出的区别,前者是在使用系统提供的内存时,做了一些超出申请的内存范围的操作;而后者则是在申请内存大小时就已超出系统能提供的。 - 缓冲区溢出
是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上,理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患.操作系统所使用的缓冲区又被称为"堆栈". 在各个操作进程之间,指令会被临时储存在"堆栈"当中,"堆栈"也会出现缓冲区溢出。 - 栈溢出
就是缓冲区溢出的一种。由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。
栈溢出就是缓冲区溢出的一种。
注意:在程序员设计的代码中包含的“内存溢出”漏洞实在太多了。导致内存溢出问题的原因有很多,比如:
(1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。
(2) 以不可靠的方式存取或者复制内存缓冲区。
(3) 编译器设置的内存缓冲区太靠近关键数据结构。
下面来分析这些因素:
- 内存溢出问题是 C 语言或者 C++ 语言所固有的缺陷,它们既不检查数组边界,又不检查类型可靠性(type-safety)。众所周知,用 C/C++ 语言开发的程序由于目标代码非常接近机器内核,因而能够直接访问内存和寄存器,这种特性大大提升了 C/C++ 语言代码的性能。只要合理编码,C/C++ 应用程序在执行效率上必然优于其它高级语言。然而,C/C++ 语言导致内存溢出问题的可能性也要大许多。其他语言也存在内容溢出问题,但它往往不是程序员的失误,而是应用程序的运行时环境出错所致。
- 当应用程序读取用户(也可能是恶意攻击者)数据,试图复制到应用程序开辟的内存缓冲区中,却无法保证缓冲区的空间足够时(换言之,假设代码申请了 N 字节大小的内存缓冲区,随后又向其中复制超过 N 字节的数据)。内存缓冲区就可能会溢出。想一想,如果你向 12 盎司的玻璃杯中倒入 16 盎司水,那么多出来的 4 盎司水怎么办?当然会满到玻璃杯外面了!
- 最重要的是,C/C++ 编译器开辟的内存缓冲区常常邻近重要的数据结构。现在假设某个函数的堆栈紧接在在内存缓冲区后面时,其中保存的函数返回地址就会与内存缓冲区相邻。此时,恶意攻击者就可以向内存缓冲区复制大量数据,从而使得内存缓冲区溢出并覆盖原先保存于堆栈中的函数返回地址。这样,函数的返回地址就被攻击者换成了他指定的数值;一旦函数调用完毕,就会继续执行“函数返回地址”处的代码。非但如此,C++ 的某些其它数据结构,比如 v-table 、例外事件处理程序、函数指针等,也可能受到类似的攻击。
1.4.2 操作系统由逻辑地址求物理地址
具体步骤 共三步
- 确定虚拟地址(物理地址)的有效位
例如:假设页面大小1KB,共32页。(页面:逻辑地址 页框:物理地址)
由32(KB)=32×1024(B) 即等于32×1024 字节
二进制用多少位能有效表示这么多字节呢——答是:15位 因为32×1024=25×210=2^15 - 再次确定逻辑地址页面位数 你应该知道:逻辑地址=页号+页面
还是以上假设,那么页面大小为1KB=1024字节 同样的方法计算出表示位数:10位
如果给你逻辑地址:0000 1111 1000 0000
那么由:011+11100000000(相当于 页号+页面(10位))推得出页号011=3 - 根据页号找出对应的页框号
由 物理地址=页框号×页块大小(页块大小是等于页面大小的)+页内位移(即页面逻辑地址)
根据上面 物理地址=页框号×1024B + 1110000000 ( 这里的相加是指位置上而言)
例如:110+110=110110(即高地址+低地址)
1.4.3 虚拟内存,虚拟地址与物理地址的转换⭐⭐⭐⭐
- 虚拟内存
虚拟内存是一种内存管理技术,它会使程序自己认为自己拥有一块很大且连续的内存,然而,这个程序在内存中不是连续的,并且有些还会在磁盘上,在需要时进行数据交换; - 优点:
可以弥补物理内存大小的不足;一定程度的提高反应速度;减少对物理内存的读取从而保护内存延长内存使用寿命; - 缺点:
占用一定的物理硬盘空间;加大了对硬盘的读写;设置不得当会影响整机稳定性与速度。 - 虚拟地址空间是对于一个单一进程的概念,这个进程看到的将是地址从0000开始的整个内存空间。虚拟存储器是一个抽象概念,它为每一个进程提供了一个假象,好像每一个进程都在独占的使用主存。每个进程看到的存储器都是一致的,称为虚拟地址空间。从最低的地址看起:程序代码和数据,堆,共享库,栈,内核虚拟存储器。大多数计算机的字长都是32位,这就限制了虚拟地址空间为4GB。
1.4.4 扇区 块 页 簇的概念 ⭐⭐
磁盘相关:磁盘IO、扇区、块与页
1.4.4.1 扇区
顾名思义,每个磁盘有多条同心圆似的磁道,磁道被分割成多个部分。每部分的弧长加上到圆心的两个半径,恰好形成一个扇形,所以叫做扇区。扇区是磁盘中最小的物理存储单位。通常情况下每个扇区的大小是512字节。(由于不断提高磁盘的大小,部分厂商设定每个扇区的大小是4096字节)
每个簇或者块可以包括2、4、8、16、32、64…2的n次方个扇区
1.4.4.2 块 簇
是操作系统中最小的逻辑存储单位。操作系统与磁盘/硬盘打交道的最小单位是磁盘块。硬盘的读写以扇区为基本单位
1.4.4.3 页
内存的最小存储单位;页的大小为磁盘块大小的2^n倍;操作系统与内存打交道的最小单位是页
1.4.5 内零头,外零头,段页式管理⭐⭐⭐⭐
操作系统在分配内存时,有时候会产生一些空闲但是无法被正常使用的内存区域,这些就是内存碎片,或者称为内存零头,这些内存零头一共分为两类:内零头和外零头。
1.4.5.1 内零头
是指进程在向操作系统请求内存分配时,系统满足了进程所需要的内存需求后,还额外还多分了一些内存给该进程,也就是说额外多出来的这部分内存归该进程所有,其他进程是无法访问的。
1.4.5.2 外零头
是指内存中存在着一些空闲的内存区域,这些内存区域虽然不归任何进程所有,但是因为内存区域太小,无法满足其他进程所申请的内存大小而形成的内存零头。
页式存储管理是以页为单位(页面的大小由系统确定,且大小是固定的)向进程分配内存的,例如:假设内存总共有100K,分为10页,每页大小为10K。现在进程A提出申请56K内存,因为页式存储管理是以页为单位进程内存分配的,所以系统会向进程A提供6个页面,也就是60K的内存空间,那么在最后一页中进程只使用了6K,从而多出了4K的内存碎片,但是这4K的内存碎片系统已经分配给进程A了,其他进程是无法再访问这些内存区域的,这种内存碎片就是内零头。
1.4.5.3 段式存储管理
段(段的大小是程序逻辑确定,且大小不是固定的)为单位向进程进行内存分配的,进程申请多少内存,系统就给进程分配多少内存,这样就不会产生内零头,但是段式分配会产生外零头。
例如:假设内存总的大小为100K,现在进程A向系统申请60K的内存,系统在满足了进程A的内存申请要求后,还剩下40K的空闲内存区域;这时如果进程B向系统申请50K的内存区域,而系统只剩下了40K的内存区域,虽然这40K的内存区域不归任何进程所有,但是因为大小无法满足进程B的要求,所以也无法分配给进程B,这样就产生了外零头。请求段式存储管理是在段式存储管理的基础上增加了请求调段功能和段置换功能。
所以段式和请求段式存储管理会产生外零头,选BD。
1.5 文件管理系统
1.5.1 硬链接与软链接的区别;⭐⭐⭐⭐⭐
解析:硬链接与软链接是常见面试题。
1.5.1.1 首先什么是链接?
链接操作实际上是给系统中已有的某个文件指定另外一个可用于访问它的名称。对于这个新的文件名,我们可以为之指定不同的访问权限 ,以控制对信息的共享和安全性的问题。 如果链接指向目录,用户就可以利用该链接直接进入被链接的目录而不用打一大堆的路径名。而且,即使我们删除这个链接,也不会破坏原来的目录。
1.5.1.2. 硬链接
硬链接只能引用同一文件系统中的文件。它引用的是文件在文件系统中的物理索引(也称为inode)。当您移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据而不是文件在文件结构中的位置。硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置,这样有助于文件的安全。如果您删除的文件有相应的硬链接,那么这个文件依然会保留,直到所有对它的引用都被删除。
1.5.1.3. 软链接(符号链接)
软连接,其实就是新建立一个文件,这个文件就是专门用来指向别的文件的(那就和windows 下的快捷方式的那个文件有很接近的意味)。软连接产生的是一个新的文件,但这个文件的作用就是专门指向某个文件的,删了这个软连接文件,那就等于不需要这个连接,和原来的存在的实体原文件没有任何关系,但删除原来的文件,则相应的软连接不可用(cat那个软链接文件,则提示“没有该文件或目录“)。
1.5.1.4. 硬链接与软链接的区别;
- 硬连接是不会建立inode的,他只是在文件原来的inode link count域再增加1而已,也因此硬链接是不可以跨越文件系统的。相反都是软连接会重新建立一个inode,当然inode的结构跟其他的不一样,他只是一个指明源文件的字符串信息。一旦删除源文件,那么软连接将变得毫无意义。而硬链接删除的时候,系统调用会检查inode link count的数值,如果他大于等于1,那么inode不会被回收。因此文件的内容不会被删除。
- 硬链接实际上是为文件建一个别名,链接文件和原文件实际上是同一个文件。可以通过ls -i来查看一下,这两个文件的inode号是同一个,说明它们是同一个文件;而软链接建立的是一个指向,即链接文件内的内容是指向原文件的指针,它们是两个文件。
- 软链接可以跨文件系统,硬链接不可以;软链接可以对一个不存在的文件名(filename)进行链接(当然此时如果你vi这个软链接文件,linux会自动新建一个文件名为filename的文件),硬链接不可以(其文件必须存在,inode必须存在);软链接可以对目录进行连接,硬链接不可以。两种链接都可以通过命令 ln 来创建。ln默认创建的是硬链接。使用-s 开关可以创建软链接。
1.5.1.5 总结:
- 软连接可以跨文件系统,硬连接不可以
- 硬连接不管有多少个,都指向的是同一个I节点,会把结点连接数增加,只有符号链接才产生新的inode节点,只要结点的连接数不是0,文件就一直存在不管你删除的是源文件还是连接的文件。只要有一个存在文件就存在。 当你修改源文件或者连接文件任何一个的时候,其他的文件都会做同步的修改。软链接不直接使用i节点号作为文件指针, 而是使用文件路径名作为指针。所以删除连接文件对源文件无影响,但是删除源文件,连接文件就会找不到要指向的文件。软链接有自己的inode, 并在磁盘上有一小片空间存放路径名。
- 软连接可以对一个不存在的文件名或者目录进行连接。
1.6 中断
1.6.1 中断和异常的区别⭐⭐⭐⭐⭐
面试考点——中断和异常的区别
答:中断是异步事件,异常是同步事件。
- 异常(同步中断) 是由 cpu内部 的电信号产生的中断,其特点为当前执行的指令结束后才转而产生中断,由于有cpu主动产生,其执行点必然是可控的。
- 中断(异步中断) 是由 cpu的外设产生的电信号引起的中断,其发生的时间点不可预期。
- 同步中断也称为异常,要么是代码错误引起的,此时cpu通过发送相关的信号来处理异常(通常可能是杀死进程的信号);要么是cpu必须处理的一些异常条件,此事cpu执行异常处理函数来恢复异常 。
1.6.2 中断怎么发生,中断处理大概流程⭐⭐⭐⭐
请求中断→响应中断→关闭中断→保留断点→中断源识别→保护现场→中断服务子程序→恢复现场→中断返回。
- 请求中断
当某一中断源需要CPU为其进行中断服务时,就输出中断请求信号,使中断控制系统的中断请求触发器置位,向CPU请求中断。系统要求中断请求信号一直保持到CPU对其进行中断响应为止。 - 中断响应
CPU对系统内部中断源提出的中断请求必须响应,而且自动取得中断服务子程序的入口地址,执行中断 服务子程序。对于外部中断,CPU在执行当前指令的最后一个时钟周期去查询INTR引脚,若查询到中断请求信号有效,同时在系统开中断(即IF=1)的情 况下,CPU向发出中断请求的外设回送一个低电平有效的中断应答信号,作为对中断请求INTR的应答,系统自动进入中断响应周期。 - 关闭中断
CPU响应中断后,输出中断响应信号,自动将状态标志寄存器FR或EFR的内容压入堆栈保护起来,然后将FR或EFR中的中断标志位IF与陷阱标志位TF清零,从而自动关闭外部硬件中断。因为CPU刚进入中断时要保护现场,主要涉及堆栈操作,此时不能再响应中断,否则将造成系统混乱。 - 保护断点
保护断点就是将CS和IP/EIP的当前内容压入堆栈保存,以便中断处理完毕后能返回被中断的原程序继续执行,这一过程也是由CPU自动完成。 - 中断源识别
当系统中有多个中断源时,一旦有中断请求,CPU必须确定是哪一个中断源提出的中断请求,并由中断控制器给出中断服务子程序的入口地址,装入CS与IP/EIP两个寄存器。CPU转入相应的中断服务子程序开始执行。 - 保护现场
主程序和中断服务子程序都要使用CPU内部寄存器等资源,为使中断处理程序不破坏主程序中寄存器的内容,应先将断点处各寄存器的内容压入堆栈保护起来,再进入的中断处理。现场保护是由用户使用PUSH指令来实现的。 - 中断服务
中断服务是执行中断的主体部分,不同的中断请求,有各自不同的中断服务内容,需要根据中断源所要完成的功能,事先编写相应的中断服务子程序存入内存,等待中断请求响应后调用执行。 - 恢复现场
当中断处理完毕后,用户通过POP指令将保存在堆栈中的各个寄存器的内容弹出,即恢复主程序断点处寄存器的原值。 - 中断返回
在中断服务子程序的最后要安排一条中断返回指令IRET,执行该指令,系统自动将堆栈内保存的 IP/EIP和CS值弹出,从而恢复主程序断点处的地址值,同时还自动恢复标志寄存器FR或EFR的内容,使CPU转到被中断的程序中继续执行。
1.7 其他操作系统常见面试题
1.7.1 大小端的区别以及各自的优点,哪种时候用,验证⭐⭐⭐⭐⭐
什么是大小端?如何确定大小端?
1)如果按照大端模式存储:从低地址到高地址:20 15 08 10
存放高字节放低地址,输出从低地址到高地址:20 15 08 10 so 结果就是 2015 810
2)如果按照小端模式存储:从低地址到高地址:10 08 15 20
存放高字节放高地址,输出从高地址到低地址:08 10 20 15 so 结果就是 810 2015
- 大端优点:
符号位在所表示的数据的内容的第一个字节中,便于快速判断数据的正负和大小 - 小端优点:
低地址放低字节,所以在强制转换时不需要调整字节的内容。而且CPU做数值运算时从内存中依次从低到高取数据进行运算直到最后刷新最高位的符号位,这样运算方式会更高效。
Ps:
1)Interl x86系列芯片使用小端存储模式,ARM的芯片默认小端,但可以切换到大端;
2)网络上普遍采用大端模式,使用大端的CPU: power pc 、DSP … …51
3)大小端是由CPU架构决定的, 不是软件决定!!!
int num = 0x12345678大端 小端例如: 0x1003高地址 78 120x1002 56 340x1001 34 560x1000低地址 12 78
#include <stdio.h>
/*联合*/
union node
{int num;char ch;
}
int main()
{union node p;//方法一p.num = 0x12345678;if (p.ch == 0x78){printf("Little endian\n");}else{printf("Big endian\n");}//方法二int num = 0x12345678;char *q = (char *)#if (*q == 0x78){printf("Little endian\n");}else{printf("Big endian\n");}return 0;
}
1.7.2 一个程序从开始运行到结束的完整过程(四个过程)⭐⭐⭐⭐
一个C语言程序到执行完文件的全过程,在linux里面用gcc编译的程序时,编译的过程可以细分为四个阶段:
1)预处理(Pre-Processing)
2)编译(Compiling)
3)汇编(Assembling)
4)链接(Linking)
源程序到可执行程序的过程。在这个过程中,会发生如下的变化:
.c文件生成.obj文件的过程,称为编译,.obj文件生成到.exe文件的过程,称为链接。.obj文件就是一个是程序编译生成的二进制文件,当.exe文件生成以后.obj文件就会被删除。
- 预处理
在预编译的过程中,主要处理源代码中的预处理指令,引入头文件,去除注释,处理所有的条件编译指令,宏的替换,添加行号,保留所有的编译器指令。
当进行预编译以后的文件中将不再存在宏,所有的宏都已经被替代。当想要判断宏是否正确或者头文件包含是否正确时,也可以通过预编译来查看。 - 编译
在预处理结束后,进行的是编译。编译过程所进行的是对预处理后的文件进行语法分析,词法分析,语义分析,符号汇总,然后生成汇编代码。 - 汇编
汇编过程将汇编代码转成二进制文件,二进制文件就可以让机器来读取。每一条汇编语句都会产生一句机器语言。 - 链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数等等。所有这些问题,都需要经链接程序的处理方能得以解决。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
编译本身也分为几个阶段:
预处理 gcc -o test.i -E test.c
编译 gcc -o test.s -S test.i
汇编 gcc -o test.o -c test.s
链接分为静态链接和动态链接:
静态链接:后缀是.a主要在编译的时候将库文件里面代码搬迁到可执行的文件中;
动态链接:后缀是.so,主要在执行的时候需要转换到库文件代码执行;
两种链接的优缺点:
(1)静态的链接产生的可执行的文件体积比较的大;而动态链接的可执行文件的体积比较小;
(2)动态的链接的编译的效率比较的高;
(3)静态链接的可执行的文件执行的效率高
(4)静态链接的可执行的文件的“布局”比较好一点;
1.7.3 堆和栈的区别⭐⭐⭐⭐⭐
堆栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。
另外堆栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。
所谓的堆栈就是数据结构里的栈或者队列,而堆和栈就是进程内存空间里的堆和栈。
下面的图总结的非常好,读者可多复习。
1.7.4 计算机中,32bit与64bit有什么区别⭐⭐⭐
解析:
参考https://blog.csdn.net/liuxingrui4p/article/details/44599773
参考http://blog.sina.com.cn/s/blog_6e6f356e0100nbt9.html
64bit计算主要有两大优点:可以进行更大范围的整数运算;可以支持更大的内存。 64位CPU一次可提取64位数据,比32位提高了一倍,理论上性能会提升1倍。但这是建立在64bit操作系统,64bit软件的基础上的。
但是我们不能因为数字上的变化,而简单的认为64bit处理器的性能是32bit处理器性能的两倍。实际上在32bit应用程序下,32bit处理器的性能甚至会更强,即使是64bit处理器,目前情况下也是在32bit应用下性能更强。所以要认清64bit处理器的优势,但不可迷信64bit。
64位操作系统下的虚拟内存空间大小:地址空间大小不是232,也不是264,而一般是248。因为并不需要264那么大的寻址空间,过大的空间只会造成资源的浪费。所以64位Linux一般使用48位表示虚拟空间地址,40位标识物理地址。0x0000000000000000~ 0x00007fffffffffff表示用户空间,0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF表示内核空间,共提供256TB(248)的寻址空间
C/C++后端开发面经(1)——计算机操作系统相关推荐
- C++后端开发面试题精选
后端开发面试题 =================== #后端开发面试知识点大纲: ##语言类(C++): ###关键字作用解释: volatile作用 Volatile关键词的第一个特性:易变性.所 ...
- c++后端开发面试题
后端开发面试题 #后端开发面试知识点大纲: ##语言类(C++): ###关键字作用解释: volatile作用 Volatile关键词的第一个特性:易变性.所谓的易变性,在汇编层面反映出来,就是两条 ...
- java 获取文件大小_阿里Java后端开发面经,面试官都替我感到绝望
点关注,不迷路:持续更新Java相关技术及资讯!!! 内容源于群友投稿!记录一次阿里Java后端开发面经,分享给大家,感谢支持! 前言 秋招面试的第一家公司,也是第一次面试,真的超级紧张,从自我介绍到 ...
- python后端开发流程_2019 Python后端开发面经总结
原标题:2019 Python后端开发面经总结 本人技术栈为Python后端开发,面经如下: python基础部分: 1. 迭代器生成器 生成器是如何实现迭代的 2. list实现 3. import ...
- 校招Java后端开发面经专栏——序
目录 前言 本专栏将包含的内容 作者以往的免费Java基础专栏 本专栏内容索引 一.基础知识 二.实战面经 三.算法 四.其他经验 后记 前言 最近从各方面了解到的消息都显示:毕业生在逐年增多,各个企 ...
- back-end 后端开发面试题
=================== 转载自:https://github.com/chankeh/cpp-backend-reference #后端开发面试知识点大纲: ##语言类(C++): # ...
- 后端怎么接收map_史上最全,C++后端开发面试题与知识点汇总
以下汇总C++后台开发面试题与知识点,还有其他岗位的相关题库和资料,想要什么岗位的可以留言哦~ 附面试题目: 一.基础知识 1.基本语言 说一下C++和C的区别 说一下C++中static关键字的作用 ...
- java后端开发面经(一)
java synchronized 对象结构:markword(8 bytes),类指针,实例对象,对齐 markword:锁信息,GC信息,hashCode 锁消除 是发生在编译器级别的一种锁优化方 ...
- Python后端开发面经
知识储备 python 后端工程师每天做什么? 网站后台业务逻辑 为网站提供API 为产品.运营提供后台网站工具,比如后台运营系统. 知识储备-上: 面试流程.技巧 通过不断的面试加深自己的面试经验 ...
最新文章
- Vue.js学习系列(四十二)-- Vue.js组件
- IsomorphicStrings(leetcode205)
- .net知识和学习方法系列(七)string类型
- mysql 数据为空 none 网页显示空白_用python爬虫爬取股票数据
- netstat 和 losf
- Jenkins 持续集成自动化测试配置
- leetcode111. 二叉树的最小深度(队列)
- LeetCode 2181. 合并零之间的节点(链表)
- assign深拷贝_Object.assign 深拷贝?浅拷贝?
- 使用Go语言实现简单MapReduce框架
- c#开发移动彩信网关
- 计算机维修难点,计算机组装与维修习重难点.doc
- decimal在java怎么用?
- 山寨起源——河神的全斧头
- 微信官方多端框架Donut可将小程序编译成 Android 以及 iOS 应用了
- Python项目:The Ship Rendezvous Problem,利用贪心算法解决船舶交会问题
- 串的一些基础操作(c语言)~DS笔记⑤
- AAA的线下保护以及路由器使用ACS认证登录
- java企业 网站源码 后台springmvc SSM 前台静态引擎 代码生成器
- 推荐一个最适合女生的冷门逆天副业!