在学习的时候找到几个十分好的工程和个人博客,先码一下,内容都摘自其中,有些重难点做了补充!
才鲸 / 嵌入式软件笔试题汇总
嵌入式与Linux那些事
阿秀的学习笔记
小林coding
百问网linux
嵌入式软件面试合集
2022年春招实习十四面(嵌入式面经)
文章目录
- 一、进程和线程
- 1.1进程和线程的概念
- 1.2 进程和线程的区别
- 1.3 进程和线程使用场景
- 1.4 进程的五种状态
- 1.4.1 如何唤醒被阻塞的socket线程?
- 1.5 创建进程的几种方式
- 1.6 进程间通信
- 1.6.1 方式
- 1.6.2 优缺点
- 1.7 线程间同步
- 1.7.1 方式
- 1.8 内核线程和用户线程
- 1.8.1 概念
- 1.8.2 区别
- 1.8.3 优缺点
- 1.9 僵尸进程,孤儿进程,守护进程
- 1.9.1 概念
- 1.9.2 如何清理僵尸进程?
- 二、堆和栈
- 2.1 为什么堆的空间是不连续的?
- 2.2 用户栈和内核栈
- 2.2.1 概念
- 2.2.2用户栈和内核栈,为什么不能共用一个栈?
- 2.2.3 线程是否具有相同的堆栈?
- 三、并发和互斥
- 3.1 概念
- 3.2 自旋锁和信号量
- 3.2.1 定义
- 3.3.2 区别
- 3.3 自旋锁和信号量可以睡眠吗?为什么?
- 3.4 自旋锁和信号量可以用于中断中吗?
- 3.5 死锁
- 3.5.1 产生死锁的原因
- 3.5.2 死锁的4个必要条件是什么?
- 3.5.3 死锁的处理方式有哪些?
- 3.5.4 如何避免死锁?
- 四、Linux系统
- 4.1 内核与系统
- 4.1.1 Linux内核的组成部分
- 4.1.2 Linux系统的组成部分
- 4.1.3 用户空间与内核通信方式有哪些?
- 4.1.4 系统调用与普通函数调用的区别?
- 4.1.5 bootloader、内核 、根文件和应用程序的关系?
- 4.1.6 Bootloader启动的两个阶段
- 4.2 程序
- 4.2.1 一个程序从开始运行到结束的完整过程(四个过程)
- 4.3 中断和异常
- 4.3.1 中断和异常的区别
- 4.3.2 什么是缺页中断?
一、进程和线程
1.1进程和线程的概念
进程
是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建。线程
是程序执行的最小单位,是进程的一个执行流,一个进程由多个线程组成的。
1.2 进程和线程的区别
- 进程是
资源分配
的最小单位。 - 线程是
程序执行
的最小单位,也是处理器调度
的基本单位,但进程不是,两者均可并发执行。 - 进程有自己的
独立地址空间
,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据
,使用相同的地址空间,因此,CPU切换一个线程的花费远比进程小很多,同时创建一个线程的开销也比进程小很多。 - 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也跟着死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
- 进程切换时,消耗的资源大,效率低。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
- 执行过程:
每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口
。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 - 线程执行
开销小
,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。进程执行开销大
,但是能够很好的进行资源管理和保护
,可以跨机器迁移。
1.3 进程和线程使用场景
- 对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
- 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
1.4 进程的五种状态
进程可以分为五个状态,分别是:
- 创建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 终止状态
创建状态
一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
就绪状态
在创建状态完成之后,进程已经准备好,但是还未获得处理器资源,无法运行。
运行状态
获取处理器资源,被系统调度,开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
阻塞状态
在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。
终止状态
进程结束或者被系统终止,进入终止状态。
进程的状态转换图
1.4.1 如何唤醒被阻塞的socket线程?
同步阻塞
- 等待锁的释放
等待阻塞
- 使用Thread.sleep造成的阻塞:时间结束后自动进入RUNNABLE状态
- 使用Thread.wait造成的阻塞:使用Thread.notify或者Thread.notifyAll唤醒
- 使用Thread.join造成的阻塞:等待上一个线程执行完后自动进入RUNNABLE状态
- 使用Thread.suspend造成的阻塞:使用Thread.resum唤醒
- 使用LockSupport.park造成的阻塞:使用LockSupport.unpark唤醒
- 使用LockSupport.parkNanos造成的阻塞:指定时间结束后,自动唤醒
- 使用LockSupport.parkUntil造成的阻塞:到达指定的时间,自动唤醒
1.5 创建进程的几种方式
创建进程的多种方式但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为个应用程序设计,比如扫地机器人,一旦启动,所有的进程都已经存在。
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4种形式
创建新的进程:
- 系统初始化(查看进程 linux中用ps命令, windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
- 一个进程在运行过程中开启了子进程(如 nginx开启多进程,os. fork, subprocess Popen等)
- 用户的交互式请求,而创建一个新进程(如用户用鼠标双击任意款软件图片:q微信暴风影音等)
- —个批处理作业的初始化(只在大型机的批处理系统中应用)无论哪-种,新进程的创建都是由—个已经存在的进程执行了—个用于创建进程的系统调用而创建的。
1.6 进程间通信
1.6.1 方式
管道(pipe)
- 管道这种通讯方式有两种限制,一是半
双工的通信
,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 - 流管道s_pipe: 去除了第一种限制,可以
双向传输(全双工)
。 - 管道可用于具有亲缘关系进程间的通信。
命名管道
:name_pipe克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系
进程间的通信(命名管道也交FIFO)。
信号量(semophore)
- 信号量是一个
计数器
,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源
。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列(message queue)
- 消息队列是由消息组成的链表,存放在内核中并由消息队列标识符标识。
- 消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号(singnal)
- 信号是一种比较复杂的通信方式,用于
通知接收进程某个事件已经发生
。主要作为进程间以及同一进程不同线程之间的同步手段。
共享内存(shared memory)
- 共享内存就是映射一段能被其他进程所访问的内存,
这段共享内存由一个进程创建,但多个进程都可以访问
。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字(socket)
- 套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
1.6.2 优缺点
管道
- 速度慢,容量有限,只有父子进程能通讯。
FIFO(First In, First Out)
- 任何进程间都能通讯,但速度慢。
消息队列
- 容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题,消息队列可以不再局限于父子进程,而允许任意进程通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息发送和接收之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便,但是信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合。此种方法不太常用。
信号量
- 不能用来传递复杂消息,只能用来同步。
共享内存
- 利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。
进程间通信方式的选择
- PIPE和FIFO(有名管道)用来实现进程间相互发送非常短小的、频率很高的消息,这两种方式通常适用于两个进程间的通信。
共享内存用来实现进程间共享的、非常庞大的、读写操作频率很高的数据;这种方法适用于多进程间的通信。
其他考虑用socket。主要应用在分布式开发中。
1.7 线程间同步
1.7.1 方式
现在流行的进程线程同步互斥的控制机制,其实是由最原始、最基本的4种方法(临界区、互斥量、信号
量和事件)实现的。
-
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程访问共享资源,如果有多个线程试图访问共享资源,那么当有一个线程进入后,其他试图访问共享资源的线程将会被挂起,并一直等到进入临界区的线程离开,临界在被释放后,其他线程才可以抢占。
-
互斥量:为协调对一个共享资源的单独访问而设计,只有拥有互斥量的线程,才有权限去访问系统的公共资源,因为互斥量只有一个,所以能够保证资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。
-
信号量:为控制一个具有有限数量的用户资源而设计。它允许多个线程在同一个时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目
-
事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
-
读写锁(Reader-Writer Lock):读写锁是一种特殊的锁,它可以分为读锁和写锁两种。当一个线程需要读取共享资源时,可以获取读锁,此时其他线程也可以同时获取读锁,但无法获取写锁;当一个线程需要修改共享资源时,需要获取写锁,此时其他线程无法获取读锁或写锁。读写锁适用于读操作远远多于写操作的场景,可以提高程序的并发性能。
-
屏障(Barrier):屏障是一种线程同步机制,它可以让多个线程在某个点上等待,直到所有线程都到达该点后再继续执行。屏障适用于多阶段任务的场景,可以保证多个线程在某个阶段完成后再开始下一阶段任务。
1.8 内核线程和用户线程
1.8.1 概念
- 用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
- 内核线程:由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。
1.8.2 区别
- 内核支持线程是OS内核
可感知
的,而用户级线程是OS内核不可感知
的。 - 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 用户级线程执行系统调用指令时将导致其所属
进程被中断
,而内核支持线程执行系统调用指令时,只导致该线程被中断
。 - 在只有用户级线程的系统内,CPU调度还是以
进程为单位
,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位
,由OS的线程调度程序负责线程的调度。 - 用户级线程的程序实体是运行在
用户态
下的程序,而内核支持线程的程序实体则是可以运行在任何状态
下的程序。
1.8.3 优缺点
内核线程的优点:
- 当有多个处理机时,一个进程的多个线程可以同时执行。
缺点:
- 由内核进行调度。
用户线程的优点:
- 线程的调度不需要内核直接参与,控制简单。
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
- 允许每个进程定制自己的调度算法,线程管理比较灵活。这就是必须自己写管理程序,与内核线程的区别
- 线程能够利用的表空间和堆栈空间比内核级线程多。
- 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
缺点:
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。
1.9 僵尸进程,孤儿进程,守护进程
1.9.1 概念
僵尸进程是一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
孤儿进程是因为父进程异常结束了,然后被1号进程init收养。
守护进程是创建守护进程时有意把父进程结束,然后被1号进程init收养
区分:一个正常运行的子进程,如果此刻子进程退出,父进程没有及时调用wait或waitpid收回子进程的系统资源,该进程就是僵尸进程,如果系统收回了,就是正常退出,如果一个正常运行的子进程,父进程退出了但是子进程还在,该进程此刻是孤儿进程,被init收养,如果父进程是故意被杀掉,子进程做相应处理后就是守护进程
1.9.2 如何清理僵尸进程?
僵尸进程的产生是因为父进程没有 wait() 子进程。所以如果我们自己写程序的话一定要在父进程中通过wait() 来避免僵尸进程的产生
。
当系统中出现了僵尸进程时,我们是无法通过 kill 命令把它清除掉的。但是我们可以杀死它的父进程,让它变成孤儿进程,并进一步被系统中管理孤儿进程的进程收养并清理。
二、堆和栈
2.1 为什么堆的空间是不连续的?
堆包含一个链表来维护已用和空闲的内存块
。在堆上新分配(用 new 或者 malloc)内存是从空闲的内存块中找到一些满足要求的合适块。所以可能让人觉得只要有很多不连续的零散的小区域,只要总数达到申请的内存块,就可以分配。
但事实上是不行的,这又让人觉得是不是零散的内存块不能连接成一个大的空间,而必须要一整块连续的内存空间才能申请成功呢。
申请和释放许多小的块可能会产生如下状态:在已用块之间存在很多小的空闲块。进而申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的,不连续的,不能满足申请的大小,这叫做“堆碎片”。
当旁边有空闲块的已用块被释放时,新的空闲块会与相连的空闲块合并成一个大的空闲块,这样就可以有效的减少"堆碎片"的产生。
堆分配的空间在逻辑地址(虚拟地址)上是连续的
,但在物理地址上是不连续的
(因为采用了页式内存管理,windows下有段机制、分页机制),如果逻辑地址空间上已经没有一段连续且足够大的空间,则分配内存失败。
综上所述,堆内存的空间不连续是由于堆内存的动态分配和释放方式所导致的。
2.2 用户栈和内核栈
2.2.1 概念
用户栈和内核栈都是计算机系统中用于处理函数调用、中断处理和系统调用等操作时保存程序状态和临时数据的栈。
-
用户栈指的是在用户空间中的栈,用于
保存应用程序的函数调用现场和临时数据
。当应用程序调用函数时,函数的参数和返回地址等数据会被压入用户栈中,函数执行完毕后,这些数据会从栈中弹出,程序会回到调用函数的位置继续执行。用户栈通常由操作系统在进程创建时分配
,每个进程
都有自己的用户栈。 -
内核栈指的是在内核空间中的栈,用于
保存内核中断处理程序和系统调用处理程序执行时的现场和临时数据
。当系统发生中断或者进行系统调用时,处理程序会被调用并执行,此时处理程序需要保存中断或者系统调用前的现场和数据,这些数据会被压入内核栈中。当处理程序执行完毕后,这些数据会从栈中弹出,系统会回到中断或系统调用前的状态继续执行。内核栈通常由操作系统在内核初始化时分配
,每个CPU核心
都有自己的内核栈。
需要注意的是,用户栈和内核栈是分开的,它们的作用范围和访问权限是不同的。用户栈只能被应用程序访问,而内核栈只能被内核代码访问。此外,用户栈和内核栈的大小也是不同的,通常内核栈比用户栈要小。
2.2.2用户栈和内核栈,为什么不能共用一个栈?
用户栈和内核栈不能共用一个栈的原因主要有以下几点:
-
权限不同:用户栈和内核栈的访问权限不同。用户栈只能被应用程序访问,而内核栈只能被内核代码访问。如果共用一个栈,由于应用程序可以直接访问栈,就有可能会破坏内核栈中的数据,导致系统出现错误或崩溃。
-
大小不同:用户栈和内核栈的大小不同。用户栈通常比内核栈大,因为应用程序需要保存更多的函数调用现场和临时数据。如果共用一个栈,就可能会出现栈溢出的情况,导致数据丢失或程序崩溃。
-
安全性问题:共用一个栈可能会导致安全性问题。由于用户栈和内核栈的数据是混合在一起的,攻击者有可能通过修改用户栈中的数据来影响内核代码的执行,从而攻击系统。如果使用不同的栈,就可以更好地保障系统的安全性。
2.2.3 线程是否具有相同的堆栈?
在计算机系统中,每个线程都有自己的堆栈,线程之间的堆栈是独立的,不共享。线程的堆栈用于存储线程函数的局部变量、函数参数、函数返回地址等数据,以及线程调用其他函数时的现场信息。
由于每个线程都有自己的堆栈,因此线程之间的堆栈是相互独立的
,不会相互干扰。这也意味着,在多线程编程中,每个线程都需要分配独立的堆栈空间。如果多个线程共享同一个堆栈空间,就会导致数据冲突和互相干扰的问题,从而引发程序崩溃等严重后果。
需要注意的是,虽然每个线程都有自己的堆栈,但是线程之间可以共享堆空间
。堆空间是由操作系统分配和管理的,不属于任何一个线程的私有空间,因此线程之间可以通过堆空间来共享数据。不过,在使用堆空间进行数据共享时,需要采取相应的同步措施来避免数据竞争和冲突的问题。
三、并发和互斥
3.1 概念
并发是指多个任务或进程可以在同一时间段内执行,它可以提高系统的效率和资源利用率。并发执行的任务或进程之间可能需要共享一些资源,如内存、文件、网络连接等。
互斥是指多个任务或进程对同一资源的访问是互相排斥的。在多任务或多进程的环境下,如果多个任务或进程同时访问同一个资源,可能会导致数据的不一致性、死锁等问题。因此,需要采用互斥控制来保证同一时间只有一个任务或进程能够访问共享资源。
解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源并发,指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
的时候,其他的执行单元都被禁止访问。
访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护。中断屏蔽,原子操作,自旋锁,和信号量
都是linux设备驱动中可采用的互斥途径。
3.2 自旋锁和信号量
3.2.1 定义
自旋锁,顾名思义,我们可以把他理解成厕所门上的一把锁。这个厕所门只有一把钥匙,当我们进去时,把钥匙取下来,进去后反锁。那么当第二个人想进来,必须等我们出去后才可以。当第二个人在外面等待时,可能会一直等待在门口转圈。
我们的自旋锁也是这样,自旋锁只有锁定和解锁两个状态。当我们进入拿上钥匙进入厕所,这就相当于自旋锁锁定的状态,期间谁也不可以进来。当第二个人想要进来,这相当于线程B想要访问这个共享资源,但是目前不能访问,所以线程B就一直在原地等待,一直查询是否可以访问这个共享资源。当我们从厕所出来后,这个时候就“解锁”了,只有再这个时候线程B才能访问。
假如,在厕所的人待的时间太长怎么办?外面的人一直等待吗?如果换做是我们,肯定不会这样,简直浪费时间,可能我们会寻找其他方法解决问题。自旋锁也是这样的,如果线程A持有自旋锁时间过长,显然会浪费处理器的时间,降低了系统性能。我们知道CPU最伟大的发明就在于多线程操作,这个时候让线程B在这里傻傻的不知道还要等待多久,显然是不合理的。因此,自旋锁只适合短期持有
,如果遇到需要长时间持有的情况,我们就要换一种方式了(互斥体)。
信号量和自旋锁有些相似,不同的是信号量会发出一个信号告诉你还需要等多久。因此,不会出现傻傻等待的情况。比如,有100个停车位的停车场,门口电子显示屏上实时更新的停车数量就是一个信号量。这个停车的数量就是一个信号量,他告诉我们是否可以停车进去。当有车开进去,信号量加一,当有车开出来,信号量减一。
比如,厕所一次只能让一个人进去,当A在里面的时候,B想进去,如果是自旋锁,那么B就会一直在门口傻傻等待。如果是信号量,A就会给B一个信号,你先回去吧,我出来了叫你。这就是一个信号量的例子,B听到A发出的信号后,可以先回去睡觉,等待A出来。
因此,信号量显然可以提高系统的执行效率,避免了许多无用功。
3.3.2 区别
- 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。
- 相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠引起的耗时可能比锁被占用的全部时间还要长。
- 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中(使用自旋锁)是不能进行调度的。
- 你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其它进程试图获得同一信号量时不会因此而死锁,(因为该进程也只是去睡眠而已,而你最终会继续执行的)。
- 在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
- 信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
- 信号量不同于自旋锁,它不会禁止内核抢占(自旋锁被持有时,内核不能被抢占),所以持有信号量的代码可以被抢占,这意味着信号量不会对调度的等待时间带来负面影响。
3.3 自旋锁和信号量可以睡眠吗?为什么?
自旋锁不能睡眠,信号量可以。
原因
自旋锁
禁止处理器抢占;而信号量
不禁止处理器抢占。
基于这个原因,如果自旋锁在锁住以后进入睡眠,由于不能进行处理器抢占,其他系统进程将都不能获得CPU而运行,因此不能唤醒睡眠的自旋锁,因此系统将不响应任何操作(除了中断或多核的情况,下面会讨论)。而信号量在临界区睡眠后,其他进程可以用抢占的方式继续运行,从而可以实现内存拷贝等功能而使得睡眠的信号量程序由于获得了等待的资源而被唤醒,从而恢复了正常的代码运行。
当然,自旋锁的睡眠的情况包含考虑多核CPU和中断的因素。自旋锁睡眠时,只是当前CPU的睡眠以及当前CPU的禁止处理器抢占,所以,如果存在多个CPU,那么其他活动的CPU可以继续运行使操作系统功能正常,并有可能完成相应工作而唤醒睡眠了的自旋锁,从而没有造成系统死机;自旋锁睡眠时,如果允许中断处理,那么中断的代码是可以正常运行的,但是中断通常不会唤醒睡眠的自旋锁,因此系统仍然运行不正常。
3.4 自旋锁和信号量可以用于中断中吗?
信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠
。
自旋锁可以用于中断。在获取锁之前一定要先禁止本地中断
(也就是本CPU中断,对于多核SOC来说会有多个CPU核),否则可能导致锁死现象的发生。
3.5 死锁
3.5.1 产生死锁的原因
死锁是多线程或多进程并发执行时经常遇到的一种问题,它发生在两个或多个进程或线程之间,每个进程或线程都在等待其他进程或线程释放其所持有的资源而无法向前执行
。死锁的产生通常有以下几个原因:
- 竞争资源:两个或多个进程或线程同时
竞争某个共享资源
,例如文件、网络连接、共享内存等。如果某个进程或线程持有了该资源,其他进程或线程就无法访问该资源,从而导致死锁。 - 竞争顺序:进程或线程在访问多个资源时,访问资源的顺序不一致,
导致互相等待
。例如,进程A持有资源X,请求资源Y,而进程B持有资源Y,请求资源X,这种情况会导致死锁。 - 缺少资源:如果系统中的
资源不足以满足所有进程或线程的需要
,就可能导致死锁。例如,如果系统只有一个打印机,而多个进程或线程都需要打印文件,就可能导致死锁。 - 饥饿:某个进程或线程
一直无法获取所需的资源
,导致一直等待,无法继续执行,这种情况称为饥饿。如果某个进程或线程一直处于饥饿状态,会导致死锁。
3.5.2 死锁的4个必要条件是什么?
死锁的4个必要条件,也称为死锁产生的充分条件,是指在满足以下4个条件的情况下,才有可能发生死锁:
- 互斥条件(Mutual Exclusion):指一个资源同时只能被一个进程或线程占用,如果一个进程或线程已经占用了该资源,其他进程或线程就不能再占用该资源。
- 请求与保持条件(Hold and Wait):指一个进程或线程在持有某些资源的同时,又请求其他进程或线程所持有的资源,而其他进程或线程又在等待该进程或线程所持有的资源。
- 不可剥夺条件(Non-preemption):指一个进程或线程已经获得了某个资源,在该进程或线程自己不释放该资源之前,其他进程或线程不能夺取该资源。
- 循环等待条件(Circular Wait):指系统中存在一个进程或线程资源的循环等待链,每个进程或线程都在等待下一个进程或线程所持有的资源。
3.5.3 死锁的处理方式有哪些?
死锁的处理方式主要从预防死锁、避免死锁、检测与解除死锁
这四个方面来进行处理。
预防死锁
- 资源一次性分配:(破坏请求和保持条件)
- 可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
避免死锁
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法
。
检测死锁
首先为每个进程和每个资源指定一个唯一的号码;然后建立资源分配表和进程等待表。
解除死锁
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
3.5.4 如何避免死锁?
下面将具体举例说明如何通过不同的措施来避免死锁的发生:
- **避免资源竞争:**例如,在多线程程序中,不同的线程可以使用不同的变量或对象,避免多个线程同时访问同一个变量或对象,从而减少资源竞争和死锁的发生。
- **避免持有资源的同时继续请求其他资源:**例如,在操作系统中,进程可以一次性请求所有需要的资源,避免在持有某些资源的同时请求其他资源,从而避免产生循环等待的情况。
- **引入资源有序分配策略:**例如,在银行家算法中,系统会按照一定的顺序分配资源,避免不同进程之间的资源竞争和循环等待。在实现时,可以设置一个资源分配的优先级,根据优先级依次分配资源,避免资源竞争和死锁。
- **引入资源剥夺机制:**例如,在操作系统中,当某个进程占用的资源被其他进程请求时,可以剥夺该进程所占用的资源,以满足其他进程的需要。在实现时,可以设置一个资源分配的策略,当某个进程请求的资源已被其他进程占用时,可以剥夺该进程的资源,以满足其他进程的需要。
- **引入资源预分配机制:**例如,在分布式系统中,可以预先分配所有需要的资源,避免了资源竞争和循环等待的情况。在实现时,可以在系统启动时,预先分配所有需要的资源,并将资源分配给不同的进程或节点,避免资源竞争和死锁。
- **采用死锁预防算法:**例如,在银行家算法中,根据进程的资源需求和系统中的资源情况,动态地分配资源,避免系统进入死锁状态。在实现时,可以通过监测进程的资源占用情况和资源请求情况,及时调整资源分配策略,避免死锁的发生。
四、Linux系统
4.1 内核与系统
4.1.1 Linux内核的组成部分
Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。
4.1.2 Linux系统的组成部分
Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。
4.1.3 用户空间与内核通信方式有哪些?
- **系统调用:**系统调用是用户空间程序向内核发起请求的标准方式。用户空间程序通过
调用系统调用接口向内核发送请求
,内核收到请求后执行相应操作并返回结果。 - proc 文件系统:proc 文件系统是一个虚拟文件系统,它提供了一种方便的方式,让用户空间程序
通过文件的形式读写内核的信息
。用户空间程序可以通过读取 /proc 文件系统中的文件来获取内核的状态信息,也可以通过写入 /proc 文件系统中的文件来修改内核的配置参数。 - sysfs 文件系统:sysfs 文件系统也是一个虚拟文件系统,它提供了一种
以属性的形式
表示设备和驱动程序的方式,用户空间程序可以通过读写 sysfs 文件系统中的属性来与内核进行通信。 - netlink 套接字:netlink 套接字是一种专门用于内核与用户空间通信的套接字,它支持
异步通信和多路复用
等特性。用户空间程序可以通过创建 netlink 套接字向内核发送请求和接收事件通知。 - ioctl 接口:ioctl 接口是一种特殊的系统调用,它允许用户空间程序向内核发起一些
特定的控制操作
。用户空间程序通过调用 ioctl 函数,向内核传递特定的命令代码和参数,来实现与内核的通信。 - procfs 文件系统:procfs 文件系统是一种虚拟文件系统,与 /proc 文件系统类似,但它提供了一种更加灵活的方式,让用户空间程序通过文件的形式读写内核的信息。与 /proc 文件系统不同的是,procfs 文件系统中的文件可以
支持任意格式的数据
,而不仅仅是文本格式。 - 共享内存:共享内存是一种特殊的
内存映射方式
,它允许用户空间程序和内核共享一块物理内存。用户空间程序可以将共享内存映射到自己的地址空间中,然后直接读写共享内存中的数据,从而实现与内核的通信。 - 系统信号:系统信号是一种
异步通知机制
,它允许内核向用户空间程序发送一些特定的事件通知。用户空间程序可以通过注册信号处理函数来处理这些事件通知,从而实现与内核的通信。 - 内核模块参数:内核模块参数是一种特殊的配置参数,它们可以被内核模块读取和修改。用户空间程序可以通过修改内核模块参数的值,来影响内核模块的行为和功能。
4.1.4 系统调用与普通函数调用的区别?
- 调用机制不同:普通函数调用是在用户空间内部完成的(无堆栈切换),而系统调用是需要将控制权从用户空间切换到内核空间(有堆栈切换),由内核完成操作,然后再将控制权返回用户空间。
- 权限限制不同:系统调用需要进行一些敏感操作,如修改系统状态、访问硬件资源等,因此
需要具有特殊的权限
。而普通函数调用则不需要特殊权限
,任何用户空间程序都可以调用它。 - 调用开销不同:由于系统调用需要将控制权从用户空间切换到内核空间,需要进行一些
额外的操作
,因此系统调用的调用开销通常比普通函数调用要高。 - 参数传递方式不同:在普通函数调用中,函数参数通常是通过
堆栈传递
的,而在系统调用中,参数通常是通过寄存器或内存
传递的。 - 返回值不同:普通函数调用的返回值通常是直接返回给调用者的,而系统调用的返回值通常是通过寄存器或内存返回给调用者的。
4.1.5 bootloader、内核 、根文件和应用程序的关系?
- Bootloader:Bootloader 是系统启动的第一阶段,它的主要作用是加载内核并将控制权转交给内核。在加载内核之前,bootloader 还可以提供一些启动参数和配置信息,例如指定内核的启动参数、指定根文件系统的位置等。
- 内核:内核是系统启动的第二阶段,它是操作系统的核心,负责管理计算机的硬件资源和提供系统调用等服务。在启动过程中,内核会通过挂载根文件系统来获取系统所需的文件和配置信息。
- 根文件系统:根文件系统是 Linux 系统的基本文件系统,它包含了操作系统的所有文件和目录,包括设备文件、配置文件、库文件和可执行文件等。在 Linux 系统中,根文件系统是由内核挂载的第一个文件系统,它通常位于硬盘的一个分区中,也可以是一个网络文件系统(NFS)。
- 应用程序:应用程序是运行在 Linux 系统中的程序,它们可以是系统自带的一些工具和服务,也可以是用户自己编写的程序。应用程序需要依赖系统的库和配置文件,这些文件通常位于根文件系统的特定目录下,例如 /usr/lib、/etc 等。
因此,bootloader、内核、根文件系统和应用程序之间的关系是:bootloader 会在系统启动时加载内核,内核会在启动过程中挂载根文件系统,并从根文件系统中读取所需的文件和配置信息,最终启动应用程序并提供相应的服务。这四个组成部分共同构成了一个完整的 Linux 系统。
4.1.6 Bootloader启动的两个阶段
Stage1:汇编语言
- 基本的硬件初始化(关闭看门狗和中断,MMU(带操作系统),CACHE。配置系统工作时钟);
- 为加载stage2准备RAM空间;
- 拷贝内核映像和文件系统映像到RAM中;
- 设置堆栈指针sp;
- 跳到stage2的入口点.
Stage2:c语言
- 初始化本阶段要使用到的硬件设备(led uart等);
- 检测系统的内存映射 ;
- 加载内核映像和文件系统映像 ;
- 设置内核的启动参数.
嵌入式系统中广泛采用的非易失性存储器通常是Flash,而Bootloader就位于该存储器的最前端,所以系统上电或复位后执行的第一段程序便是Bootloader。
4.2 程序
4.2.1 一个程序从开始运行到结束的完整过程(四个过程)
预处理(Pre-Processing)、编译(Compiling)、汇编(Assembling)、链接(Linking)。
4.3 中断和异常
4.3.1 中断和异常的区别
- 触发方式不同:中断是由
外部设备或者其他程序
发起的,用于通知 CPU 处理某个事件,例如键盘输入、网络数据到达等。而异常则是由CPU 内部发起
的,通常是因为程序执行出现了错误或者非法操作,例如除以零、访问非法内存等。 - 处理方式不同:当 CPU 接收到中断信号时,它会暂停当前任务,保存当前上下文,转而处理中断事件。而当 CPU 接收到异常信号时,它会立即停止当前任务,保存当前上下文,并跳转到异常处理程序。异常处理程序通常会尝试修复错误,并恢复程序的执行。
- 响应时间不同:中断通常需要等待外部设备或者其他程序发出信号,而异常通常发生在程序的执行过程中,几乎是实时发生的。
- 处理结果不同:中断处理完成后,CPU 会回到之前的任务继续执行。而异常处理完成后,程序可能会继续执行,也可能会被终止,这取决于异常的类型和处理方式。
4.3.2 什么是缺页中断?
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
- 保护CPU现场;
- 分析中断原因;
- 转入缺页中断处理程序进行处理;
- 恢复CPU现场,继续执行。
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
- 在指令执行期间产生和处理缺页中断信号
- 一条指令在执行期间,可能产生多次缺页中断
- 缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。