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