嵌入式开发工程师笔试面试指南-操作系统-1

一 操作系统基本特性⭐⭐⭐⭐⭐

并发,并发是系统中程序能够并发执行的特征,可使得操作系统有效提高系统中的资源利用率,增加系统的吞吐量

并发和并

并发:两个或者多个事件在同一时刻发生。

并行:两个或者多个时间在同一时间间隔内发生。在多道程序下,并发性是指在一段时间内宏观上有多道程序同时运行,但是单处理机系统中,每一时刻只能有一道程序执行。

共享 在操作系统环境下的资源共享成为资源复用,是指系统中资源可供内存中多个并发执行的进程共同使用

互斥共享和同时访问

互斥共享:系统的某些资源,虽然可以提供给多个进程使用,但应规定在一段时间内,只允许一个进程访问资源。

同时访问:系统中还有另一类资源,允许在一段时间内由多个进程同时对他们进行访问

并发和共享是操作系统的两个最基本的特征,它们互为存在的条件。一方面资源共享是以进行并发执行作为条件的,若系统不允许并发执行也就不存在资源共享问题;另一方面,若系统不能对资源共享实施有效管理,以协调好诸进程对资源共享的访问,也必然会影响到诸进程并发执行的程度,甚至无法并发执行。

虚拟 通过某种技术将一个物理实体变为若干个逻辑上对应的物的功能。在操作系统中可以通过时分复用技术和空分复用技术来实现虚拟的

时分复用技术:可利用某设备为一用户服务的空闲时间,又转去为其他用户服务,使设备得到最充分的利用。

空分复用技术:利用存储器的空闲空间分区域存放和运行其他的多道程序,以此来提高内存的利用率。

异步 在多道程序环境下,系统允许多个进程并发执行。在单处理机程序环境下,由于系统只能运行一个进程,其余进程只能等待。由于资源等因素的限制,使进程的执行通常不是一气呵成的,而是走走停停的方式运行。

二 操作系统主要功能⭐⭐⭐⭐⭐

操作系统是计算机系统中最重要的一个部分,它的主要功能如下:

管理计算机的资源。包括处理器、内存、硬盘、输入输出设备等资源的管理和分配,保证资源的有效利用,避免资源的浪费和冲突。

提供用户接口。操作系统通过图形界面、命令行等方式为用户提供交互式的接口,接受并处理用户的指令和请求。

管理文件和目录。操作系统提供文件系统管理功能,将磁盘空间划分为若干个文件存储区域,对文件进行管理和维护,提供文件的访问、读、写、修改、删除等功能。

实现进程管理。操作系统对进程进行管理,包括对进程的创建、调度、切换、终止等操作,保证进程的正常运行。

实现系统安全。操作系统能够保证系统的安全性和稳定性,包括用户身份认证、进程之间的安全控制、文件系统访问控制、病毒和恶意软件的防范等。

实现网络通信。现代操作系统通常都具备网络通信功能,能够支持各种网络协议及其通信,提供各种通信服务。

总之,操作系统是计算机系统中最为关键的一部分,它提供了丰富的功能和服务,能够有效地管理计算机资源、提供用户接口、保证系统安全和稳定性,是现代计算机系统不可或缺的组成部分

三 CPU工作原理⭐⭐⭐⭐⭐

CPU的运行原理就是:控制单元在时序脉冲的作用下,将程序计数器里所指向的指令地址送到地址总线上去,然后CPU将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后CPU把数据读到CPU的内部存储单元(就是内部寄存器)暂存起来,最后命令运算单元对数据进行处理加工。这个过程不断重复,直到程序结束

四 CPU流水线⭐⭐⭐⭐⭐

CPU执行一条指令时,分为几个步骤,CPU并不会等一条指令完全执行完才执行下一条指令,而是像流水一样。

经典MIPS五级流水线将执行的生命周期分为五个部分:

取指

译码

执行

访存

写回

五 内核态和用户态的区别⭐⭐⭐⭐⭐

内核态和用户态是操作系统中的两种不同的运行态,它们的主要区别在于权限和特权级别的不同。

内核态(又称核心态、特权态)是操作系统的特权级运行态,只有操作系统内核本身才能够运行在内核态。在内核态运行时,操作系统拥有完全的管理权限和访问计算机硬件资源的权利,能够执行任何操作。

用户态(又称普通态、非特权态)是应用程序的运行态,应用程序只能运行在用户态。在用户态下,应用程序执行的指令和操作受到限制,不能直接访问和操作计算机硬件资源,必须通过操作系统提供的接口或系统调用来访问硬件资源

什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的

总之,内核态和用户态的主要区别在于操作系统的权限和特权级别不同。在内核态下,操作系统拥有完全的权限和访问硬件资源的权利;而在用户态下,应用程序只能受到限制的操作,需要通过操作系统提供的特定接口来访问硬件资源。操作系统能够很好地利用这种不同特权级别来保证计算机系统的安全和稳定性,防止应用程序对计算机系统造成不必要的损害。

五 系统调用⭐⭐⭐⭐⭐

系统调用(System call)是操作系统提供给应用程序的一组接口,用于让应用程序访问操作系统提供的服务和资源。当应用程序需要访问操作系统底层的硬件资源或进行一些特殊的操作时,就需要通过系统调用发出请求,由操作系统代表应用程序完成请求的操作,操作系统将执行结果返回给应用程序。

系统调用可以被看作是应用程序与操作系统之间的桥梁。应用程序通过系统调用接口向操作系统发出请求,一旦请求被接收,操作系统就会在内核态下执行相应的操作,然后将结果返回给应用程序。系统调用是操作系统提供的底层接口,它能够访问各种硬件资源和系统资源,如文件系统、进程管理、网络通信等

常见的系统调用包括:

文件 IO 系统调用:open, read, write, close 等

进程控制系统调用:fork, execve, getpid, wait 等。

网络通信系统调用:socket, bind, listen, accept, connect 等。

总之,系统调用是操作系统提供的向应用程序提供底层服务和操作系统资源的接口。它为应用程序提供了丰富的功能和服务,是操作系统和应用程序之间交互的主要方式之一。

六 物理内存层次⭐⭐⭐⭐⭐

物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘

寄存器:速度最快、量少、价格贵。

高速缓存:次之。

主存:再次之。

磁盘:速度最慢、量多、价格便宜。

操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存

七 存储类型⭐⭐⭐⭐⭐

硬件存储器是指计算机系统中用来存储数据和程序的物理设备,如内存、硬盘、光盘等。硬件存储器按照工作原理和使用方式可以分为不同的类型

随机存取存储器(RAM):是一种易失性存储器,用于临时存储程序和数据。RAM 存储器存取速度快,但电源关闭后存储的信息被释放,不能永久存储。

只读存储器(ROM):是一种只能读取不能写入的存储器,用于存放计算机系统的基本信息、固件和程序,如BIOS和操作系统等。

可编程只读存储器(PROM):是一种可编程的只读存储器,通过烧录来存储程序和数据。

闪存存储器(Flash Memory):是一种非易失性存储器,可作为计算机系统永久存储的媒介,如固态硬盘、USB闪存盘等。具有容量大、读取速度快、传输速率高等优点,已逐渐替代了旧的硬盘和光盘存储器。

磁盘存储器:包括硬盘和软盘,是常用的存储设备之一,可以永久存储数据并较容易被更新。

光盘存储器:包括CD、DVD等,是存储容量较大的永久性存储设备,不易受到磁场干扰,可长期保存。

除此之外,还有一些特殊的硬件存储器类型,如高速缓存存储器(Cache Memory)等,它们都有着不同的特征和使用方式,对于特定的计算机应用场景,需要根据实际需求进行选择和使用。

八 进程⭐⭐⭐⭐⭐

进程的定义

进程是指在计算机中执行的一个程序。每个进程都有自己的内存空间和系统资源,如文件、端口、线程等。进程是计算机操作系统中的一个基本概念是操作系统进行资源分配和调度的最小单位每个进程都有一个唯一的标识符(进程号),可以在操作系统中进行管理和控制

在操作系统中,每个进程都有自己的地址空间和指令和数据,这个地址空间通常由内核进行分配和管理。在运行时,进程可以创建和销毁多个线程,每个线程可以在进程的地址空间中独立运行。

每个进程可分为以下几个部分:

程序代码区:包含执行程序的指令代码

数据区:包含程序执行时所需的数据

:动态分配的内存空间

:运行时函数调用、返回和局部变量存储的空间

操作系统通过进程控制块(Process Control Block,PCB)来记录和管理每个进程的状态、所有权等信息,每个进程的PCB包含进程标识符、进程状态、CPU寄存器状态、内存分配状态、打开文件状态信息等。操作系统可以通过PCB来进行进程的调度和管理,以保证计算机系统的高效运行

进程的特征

进程具有以下特征:

动态性:进程是动态产生和消失的,系统中可以同时存在多个进程,新的进程可以随时被创建,而已有的进程也可以随时被销毁。

独立性:每个进程都有自己的内存空间和系统资源,进程之间不会相互干扰。

并发性:多个进程可以同时运行,计算机系统可以快速地进行多任务处理,提高效率,有利于实现多人共享系统资源。

异步性:不同进程的执行速度不同,各个进程之间的执行顺序和时间不受约束,执行顺序和速度都是不可预知的。

独立的地址空间:每个进程有自己的地址空间,进程之间的数据是相互隔离的,提高了计算机系统的安全性。

同步与通信:不同的进程之间可以进行通信,可以利用操作系统提供的信号量、共享内存、管道等机制来实现进程间的同步和通信。

共享与互斥:在进程并发执行时,可能会有多个进程要对同一资源进行访问,通过锁机制实现资源的互斥访问,避免了资源冲突和死锁问题。

进程的五种基本状态及转换

进程有五种基本状态:创建状态、就绪状态、运行状态、阻塞状态和终止状态

创建状态:当操作系统接受到用户进程创建的请求时,会为进程分配资源,并将进程的PCB加入到系统的进程队列中。

就绪状态:当进程具备运行条件时,它被加入就绪队列中,等待操作系统为其分配CPU执行时间。

运行状态:当进程获得CPU执行权后,进程开始运行,执行其指令,直到结束或者进入阻塞状态。

阻塞状态:当进程在运行时需要等待其它的事件发生,比如等待I/O的完成,或者等待系统调用的结果,进程会从运行状态转换到阻塞状态。

终止状态:当进程完成其任务或者因为异常情况终止时,进程会从任何状态转移到终止状态,回收并释放系统资源。

进程状态之间的转换如下:

从创建状态到就绪状态:系统为进程分配资源后,进程进入就绪状态,等待系统分配CPU执行时间。

从就绪状态到运行状态:当进程获得CPU执行资格时,进程进入运行状态,开始执行。

从运行状态到就绪状态:当进程执行完其指令后, 或者因为时间片用完而被中断时,进程从运行状态转换到就绪状态。

从运行状态到阻塞状态:当一个进程在执行中需要等待其它事件的发生,比如等待I/O完成时,进程会从运行状态转换为阻塞状态,等待事件完成。

从阻塞状态到就绪状态:当一个进程等待的事件完成时,它就转移到就绪状态,等待系统重新为其分配CPU执行时间。

从运行状态到终止状态:当进程执行完成或者因为异常情况终止时,它就转移到终止状态,系统回收并释放该进程所占有的资源。

创建进程的方式?

创建进程的方式可以分为进程的创建和进程的替换两种情况。

一、进程的创建(fork)

在 UNIX/Linux 系统中,可以使用 fork 系统调用来创建一个新的进程。系统会复制一份与父进程相同的子进程,但是父子进程之间的内存空间是独立的,父子进程之间通过进程间通信的方式进行数据交换和通信。在 fork 成功后,父进程和子进程各自返回不同的值,而且每个进程会拥有自己唯一的进程 ID。

二、进程的替换(exec)

进程的替换是指当前进程被另一个程序替换成新的进程,这个新进程会完全接管原有的进程的资源,包括进程 ID 等。在 UNIX/Linux 系统中,可以使用 exec 系统调用来进行进程的替换。

一般来说,可以先使用 fork 系统调用创建一个新的进程,然后再使用 exec 系统调用加载新的程序代码,从而实现进程的替换。这样可以保证在进程替换后,依然保留原进程的一些状态和资源信息。

进程控制

进程控制指的是操作系统通过对进程的调度、调度策略、进程优先级等方式来控制进程的执行。进程控制由操作系统进行,是操作系统对进程状态的监控和管理。具体的控制方式包括以下几种:

进程调度:指操作系统在多个就绪进程中选取一个进程分配CPU执行时间的过程。进程调度的目的是提高CPU的利用率和系统的响应速度。

进程同步:多个进程之间为了协作完成某一任务,必须要进行同步。操作系统提供了各种机制,如信号量、互斥锁、条件变量等来实现进程间的同步。

进程通信:进程通信是指多个进程之间交换信息和共享数据的过程。进程通信是通过操作系统提供的IPC机制来实现的,如管道、消息队列、共享内存、Socket等。

进程安全:进程安全指保证进程能够正确、合法的执行,并防止进程间相互干扰和冲突的一组机制。进程安全是操作系统对进程进行的重要控制之一,其技术手段包括:鉴权、进程隔离、进程权限控制等。

死锁处理:在操作系统中,进程之间可能会因为资源的竞争和协作等原因产生死锁。操作系统提供了死锁预防和死锁解除的机制,如银行家算法、超时机制、资源剥夺等方式来避免和解除死锁。

进程同步方式

在多进程操作系统中,由于多个进程都需要共享系统资源,同时对这些资源进行访问可能会产生冲突,因此需要进行同步与互斥。常见的进程同步方式包括:

信号量: 信号量是一个整形变量,可以用来控制多个进程对共享资源的访问。在信号量机制中,对共享资源进行互斥访问的进程会对信号量进行P(proberen)操作,而释放共享资源的进程则进行V(verhogen)操作。当信号量为1时,表示共享资源可以被访问,为0时表示已经被占用。

互斥锁:互斥锁是一种二元信号量,只有0和1两种状态,可以用来保证多个进程互斥地访问共享资源。只有获取到锁的进程才能访问共享资源,其他进程需要等待锁的释放。当锁被占用时,其他进程对锁的请求会被阻塞,直到锁被释放。

条件变量:条件变量可以用来实现进程间的同步和通信。条件变量通常与互斥锁一起使用,当共享资源的某个条件未得到满足时,会阻塞等待,当获取到满足条件的资源时,可以通过条件变量通知阻塞的进程获取共享资源。

读写锁: 读写锁是一种特殊的锁,用于在数据读取操作多于写入操作的情况下提高并发性能。具体来说,读写锁允许多个读者访问,但只允许一个写者访问。当写操作正在被执行时,所有读和写操作都将被阻塞。

消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。

管程:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。

总之,进程同步是保证多进程共享资源的正确使用的关键,以上介绍的同步方式只是提供了一些常见的机制,实际上还有很多不同的技术可以用于进程同步

进程通信方式

在多进程操作系统中,进程通信是实现多个进程之间交换数据和信息的一种非常重要的机制。常见的进程通信方式包括:

管道: 管道是一种进程间的半双工数据通信方式,有两种类型:匿名管道和命名管道。匿名管道是一种无名的单向管道,只能在有亲缘关系的进程之间使用;命名管道是一种有名的单向FIFO管道,允许无亲缘关系的进程之间使用。

消息队列: 消息队列是一种消息传递机制,进程可以通过向消息队列发送消息来实现与其他进程的通信。消息队列通过一个标识符进行标识,允许不同的进程通过该标识符进行访问。

共享内存: 共享内存是一种高效的进程通信方式,多个进程可以通过共享内存来访问同一份数据。共享内存允许多个进程在其地址空间中映射同一块物理内存,从而在各进程间实现数据的共享。

信号: 信号是一种异步通信机制,用于向进程发送通知。进程不需要在任何时候等待信号的到来,它可以在任何时候处理信号的到来。信号可以用于进程间的通信,但是仅仅限于一些标准信号,通讯是不可靠的。

Socket: Socket 是一种跨机器和跨平台的进程间通信方式,是应用层通信接口(API)的一种实现方式,可以在不同的计算机间通信。Socket 的实现方式包括 TCP、UDP、IP 等协议。

综上所述,不同的进程通信方式各有优劣,并且根据实际需要选择合适的进程通信方式进行开发。

进程通信中的管道实现原理是什么?

操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间同一时刻进行单向通信的机制。因为这种特性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指同一时刻数据只能由一个进程流向另一个进程(一端负责读,一端负责写);如果是全双工通信,需要建立两个管道。

管道分为无名管道和命名管道,无名管道只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件,管道本质是一种文件;命名管道可以允许无亲缘关系进程间的通信。

管道原型如下:

#include <unistd.h>  
int pipe(int fd[2]);  

管道两端可分别用描述字fd[0]以及fd[1]来描述。注意管道的两端的任务是固定的,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另 一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将发生错误。一般文件的 I/O 函数都可以用于管道,如close()、read()、write()等

通信是指两个进程之间的信息交互,而pipe()函数创建的管道处于一个进程中间,单个进程中的管道几乎没有任何用处。因此一个进程在由 pipe()创建管道后,一般再使用fork() 建立一个子进程,然后通过管道实现父子进程间的通信。父子进程都有读端和写端,子进程的是从父进程复制过来的

具体步骤如下:

父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端

父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道

父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信

什么是信号量,有什么作用?

概念信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻可以有多个进程对资源进行访问

原理由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:

(1)P(sv)操作:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。

(2)V(sv)操作:如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态,直到信号量的值大于0,若进程被唤醒则返回至第一步)。

作用用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻可以有多个进程对资源进行访问。

多进程内存共享可能存在什么问题?如何处理?

内存共享fork父子进程代码段共享,专门设立一块数据内存让两个进程共享,就实现了两个进程的信息互通

但是也会出现一个问题,并发时,一个修改数据,一个正在读数据,自然会出bug

还要考虑共享内存时访问的同步问题。比如加入互斥锁或者信号量实现同步

互斥量能不能在进程中使用?

不同的进程之间,存在资源竞争或并发使用的问题,所以需要互斥量

进程中也需要互斥量,因为一个进程中可以包含多个线程,线程与线程之间需要通过互斥的手段进行同步,避免导致共享数据修改引起冲突。可以使用互斥锁,属于互斥量的一种

Linux的fork的作用

fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。

fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:

(1) 对于父进程,fork()函数返回新创建的子进程的PID

(2) 对于子进程,fork()函数调用成功会返回0

(3) 如果创建出错,fork()函数返回-1

fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况

什么是守护进程,如何创建?

守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务

创建过程如下:

创建子进程,终止父进程

调用setsid()创建一个新会话

将当前目录更改为根目录

重设文件权限掩码

关闭不再需要的文件描述符

孤儿进程与僵尸进程,如何解决僵尸进程

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

僵尸进程,是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程

所以两者的区别是:孤儿进程是父进程已退出,子进程未退出;而僵尸进程是父进程未退出,子进程已退出

如何解决僵尸进程

(1)一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的

(2)使用kill命令。

打开终端并输入下面命令:

$ ps aux | grep Z

会列出进程表中所有僵尸进程的详细内容。

然后输入命令:

$ kill -s SIGCHLD pid(父进程pid)

这样子进程退出后,父进程就会收到信号了。

或者可以强制杀死父进程:

$ kill -9 pid(父进程pid)

这样父进程退出后,这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。

九 线程⭐⭐⭐⭐⭐

线程的定义

线程是操作系统中调度的基本单位,一个进程可以包含多个线程。线程是一条执行路径,有自己的程序计数器、寄存器和栈,但没有自己的地址空间和全局变量等资源,它们共享所属进程的资源和数据

线程与进程不同,不需要独立的内存空间和系统资源,因此线程之间的切换比进程之间的切换更加快速和经济多线程编程是一种利用多线程并发执行来提高程序性能和效率的编程模型。线程可以并行执行,这意味着多个线程可以同时执行不同的任务。在多核处理器上,多个线程可以同时运行,进一步提高了应用程序的性能

多线程可以提高程序的响应速度和并发性能,也可以实现程序内部的并行计算,但多线程编程也会增加程序的复杂性,需要仔细地处理线程间的同步和通信等问题。线程间的共享数据也是一个需要特别注意的问题,需要使用特殊的同步机制来保证数据的正确性。

线程的实现

线程的实现通常由操作系统提供支持,主要通过线程库来实现。操作系统提供了一些系统调用来创建、启动和管理线程,而线程库则提供了一些封装和抽象,使得程序员可以更方便地使用线程

线程库通常提供了一些函数接口,例如 pthread 库(POSIX 线程库)提供了一些创建、退出、调度、同步以及线程属性管理等函数

pthread_create: 创建线程

pthread_exit: 结束线程

pthread_join: 等待线程结束

pthread_mutex_lock/pthread_mutex_unlock: 互斥锁加锁/解锁操作

pthread_cond_wait/pthread_cond_signal: 等待条件变量/发送信号到条件变量

线程库的实现通常通过操作系统提供的原语,封装了一些有用的线程控制和同步功能,可以实现多线程执行和线程间的通信。通过线程库,程序员可以更加方便地使用线程,而不需要直接与操作系统底层的线程调度机制打交道。

进程、线程、协程是什么,区别是什么?

进程、线程和协程都是并发编程中常用的概念,它们各自有自己的特点和用途,下面是它们的简单介绍和区别:

进程(Process)

进程是操作系统中分配资源和调度的基本单位,它是正在运行的一个程序。每个进程拥有自己的地址空间、代码段、堆栈、数据段、进程控制块(PCB)等资源,进程之间内存独立,相互独立运行,进程间通信需要通过进程间通信(IPC)实现

线程(Thread)

线程是进程的执行单元,是比进程更轻量级的调度单位。一个进程可以包含多个线程,同一进程内的多个线程共享进程的地址空间和资源,但每个线程有自己的堆栈、程序计数器、局部变量等值。线程之间通过共享的变量或信息进行通信或同步

协程(Coroutine

协程是一种用户级的轻量级线程,也称为微线程。协程可以在同一个线程中,通过协作式任务切换实现多任务并发。一个协程可以暂停执行,保存其当前状态,稍后重新恢复执行,协程之间通过yield和resume等语义来协作执行,可以更加细粒度的控制程序执行顺序

区别:

进程拥有独立的地址空间和全局变量等资源,线程和协程则可以共享资源

进程是操作系统中资源分配的基本单位,线程是调度的基本单位,协程可以在用户态中实现调度

进程之间通信需要通过进程间通信(IPC),线程之间通信可以通过共享变量或信息实现,协程之间通信也是共享变量或信息

线程和协程通常比进程轻量级,创建和上下文切换的开销较小,因此在处理大量并发任务时,相较于进程更加高效。协程比线程更加轻量与灵活,可以避免锁竞争、上下文切换等开销。

协程是轻量级线程,轻量级表现在哪里?

协程调用跟切换比线程效率高:协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。

协程占用内存少:执行协程只需要极少的栈内存(大概是4~5KB),而默认情况下,线程栈的大小为1MB

切换开销更少:协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少

线程间通信的方式有哪些?

线程间的通信方式包括互斥量、信号量、条件变量、读写锁:

互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。

信号量:计数器,允许多个线程同时访问同一个资源。

条件变量:通过条件变量通知操作的方式来保持多线程同步。

读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。

线程同步方式有哪些?

线程间的同步方式包括互斥锁、信号量、条件变量、读写锁

互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。

信号量:计数器,允许多个线程同时访问同一个资源。

条件变量:通过条件变量通知操作的方式来保持多线程同步。

读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。

进程和线程的区别?

进程(Process)和线程(Thread)都是操作系统中的概念,主要用于描述运行中的程序。

进程指的是一个执行中的程序,它是系统资源分配的基本单位,拥有独立的内存空间、数据栈、文件句柄和其他系统资源,进程间相互独立,可互相通信。在操作系统中,每个进程都有一个唯一的进程ID,可以通过这个ID来识别和管理进程

线程指的是一个进程中的一个执行单元,是CPU调度和分派的基本单位,也是系统资源分配的基本单位。与进程相比,线程更轻量级,线程间通信比进程更加高效,线程之间共享同一进程的内存空间和其他系统资源,所以线程之间的切换速度比进程之间的切换速度更快

总起来说,所谓进程,就是一个具有一定独立功能的程序关于某个数据集合上的一次运行活动,它是操作系统进行资源分配、调度和运行的基本单位;而线程则是进程的一个执行实例,是比进程更小的独立运行的基本单位。

有了进程,为什么还要有线程?

原因

进程在早期的多任务操作系统中是基本的执行单元。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。但是进程频繁切换将引起额外开销,从而严重影响系统的性能。为了减少进程切换的开销,人们把两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这就是线程

线程与进程对比

(1)进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。

但多个线程共享进程的内存,如代码段、数据段、扩展段,线程间进行信息交换十分方便。

(2)调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。

但创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表

单核机器上写多线程程序,是否要考虑加锁,为什么?

在单核机器上写多线程程序,仍然需要线程锁

原因:因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突

多线程和多进程的不同?

(1)一个线程从属于一个进程;一个进程可以包含多个线程。

(2)一个线程意外死亡,可能导致进程挂掉,多线程也可能挂掉;一个进程挂掉,不会影响其他进程,多进程稳定。

(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。

(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。

(5)多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。

(6)通信方式不一样。

(7)多进程适应于多核、多机分布;多线程适用于多核。

进程和线程相比,为什么慢?

进程系统开销显著大于线程开销;线程需要的系统资源更少。

进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。

进程通信比线程通信开销大。进程通信需要借助管道、队列、共享内存,需要额外申请空间,通信繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。

什么是内核线程和用户线程?

内核线程和用户线程都是操作系统中的线程概念,它们之间最主要的区别在于受谁管理

内核线程(Kernel Thread)是由操作系统内核管理和调度的线程,所有的内核线程都是在内核态下运行的,它们具有访问系统资源的能力,是操作系统的核心部分之一。内核线程通常由操作系统自身创建并管理,不依赖于任何特定的应用程序,所以在内核线程中可以访问所有的硬件设备和系统资源,是操作系统实现各种服务功能的基础

用户线程(User Thread)是由应用程序程序自身创建和管理的线程,所有的用户线程都是在用户态下运行的,它们不能直接访问系统资源和硬件设备,只能通过系统调用向操作系统请求服务和资源。用户线程的创建和管理都由应用程序自身完成,所以用户线程和应用程序之间具有相对独立的关系

在实际应用中,内核线程和用户线程大多数存在于混合型多线程应用程序中。一般来说,应用程序会将若干个用户线程映射到一个或多个内核线程上来执行,从而兼顾了用户线程的轻量化和内核线程的高效性能。

如何实现线程池?

线程池主要包含以下几个部分:任务队列、线程池管理器、工作线程和任务等。实现线程池需要以下几个步骤:

  1. 初始化线程池管理器,包括创建指定数量的工作线程,构建任务队列等。
  2. 向任务队列中添加任务。
  3. 工作线程从队列中取出任务并执行。
  4. 每个工作线程都要一直循环工作,直到被停止。
  5. 线程池管理器需要时刻监控线程池中的状态,维护线程池的大小,动态增加或减少线程的数量。

以下是一个简单的线程池实现示例:

#include <iostream>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>

using namespace std;

class ThreadPool {
public:
    ThreadPool(size_t size) {
        if (size < 1) size = 1; // 至少有一个线程
        for (size_t i = 0; i < size; ++i) {
            threads.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this]() { return stop || !tasks.empty(); }); // 等待有任务或停止事件
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (auto &thread : threads)
            thread.join();
    }

    template<class F>
    void push(F &&f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }

private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop = false;
};

在这个示例代码中,我们使用std::vector<std::thread>来保存线程池中的所有工作线程,使用std::queue<std::function<void()>>来保存所有的任务。在实现中,每个线程都会不断从任务队列中获取任务执行,如果队列为空,则线程会进入等待状态。同时,线程池还支持动态添加任务,当有新的任务加入任务队列时,线程池会自动唤醒一个线程进行处理。最后,当需要销毁线程池时,线程池会通知所有工作线程停止工作,并等待所有线程完成任务。

十 进程调度算法 ⭐⭐⭐⭐⭐

先来先服务调度算法

先来先服务(FCFS,First Come First Served)算法是操作系统中最早出现的进程调度算法之一,也是最简单的一种算法。它的工作原理是按照进程到达的时间先后,依次运行,直到当前进程结束或者阻塞,才会运行下一个进程

在FCFS算法中,如果一个进程持续时间过长,会导致其他进程等待时间过长,从而影响系统整体的响应速度。缺点是无法保证短作业优先,因为长作业的到来会阻塞短作业的运行,从而导致平均等待时间和平均周转时间较长。

FCFS算法可以比较容易地实现和理解,但是对于一个实际的操作系统而言,它仍然具有许多问题。因此,FCFS算法现在在大多数情况下都只是作为其他进程调度算法的基础,而不是作为一种真正的优化算法使用。

短作业(进程)优先调度算法

短作业优先调度算法(SJF,Shortest Job First)是一种进程调度算法,通常是根据进程需要的CPU时间来排序调度的。它是一种非抢占式算法,在任意时间片都不会剥夺正在执行的进程,直到该进程完成或发生阻塞

SJF算法可以保证优先短作业的运行,减少了平均等待时间和平均周转时间。如果没有新的短作业到达,SJF算法可以获得最佳的平均等待时间和平均周转时间。

但是,短作业优先调度算法存在一个很大的问题,即不利于长作业的处理。若有一个长作业在前面占据了一个核心,而后面的短作业们需要等待很长时间,这就导致了短作业在等待长作业时浪费了很多时间。这种现象称为“饥饿状态”

因此,最优的策略是优先调度短作业,但是也不能完全无视长作业的存在,以防止CPU资源的浪费。因此,在SJF算法中通常进行一定的调整,例如加入抢占机制等,以便更好地处理长短作业间的关系。

高优先级优先调度算法

高优先级优先调度算法是一种进程调度算法,它按照优先级高低划分为多个队列,每个队列内的进程按照先来先服务的原则进行调度,优先级高的进程先被执行。如果有多个进程拥有相同的优先级,则按照先来先服务的原则进行调度

该调度算法的优点是可以保证高优先级进程优先被服务,缩短高优先级进程的响应时间。但它也存在一些缺点,如可能出现低优先级进程一直得不到调度的情况,从而容易引发饥饿问题

这种调度算法通常应用于实时操作系统中,对于那些对响应时间要求比较高的任务进行处理,例如一些需要及时响应的控制系统、测控系统等。

时间片轮转法

时间片轮转法是一种常见的进程调度算法。它将CPU的时间分成一个个时间片,每个进程被分配一个固定大小的时间片。当轮到该进程运行时,它只能运行一个时间片,时间片结束后CPU将转移到下一个进程

如果当前进程在一个时间片之内没有执行完,它就会被暂停,被放到一个就绪队列的末尾,等待下一次的调度。这样,每个进程都能获得一定数量的CPU运行时间,避免了长时间等待的饥饿状态

时间片轮转法能够保证每个进程都有机会获得CPU时间,是一种比较公平的调度算法。同时,它还可以兼顾短作业和长作业,因为长作业能够在多个时间片内完成

然而,时间片的大小也是一个需要考虑的问题。时间片过小,会导致频繁的上下文切换,会浪费大量的CPU时间;时间片过大,会导致响应时间较长,不利于及时响应一些实时任务

因此,为了兼顾各种因素,实际中通常需要根据具体情况来设置时间片的大小。

多级反馈队列调度算法

多级反馈队列调度算法是一种比较复杂的进程调度算法,它将进程按照运行时间长短分配到多个队列中,每个队列都有不同的优先级。一般来说,初始时将进程分配到优先级最低的队列,每个队列的优先级顺序逐渐升高

在多级反馈队列调度算法中,每个队列都有一个时间片,进程在一个队列内运行的时间片用完之后,如果没有完成任务,它将会被移动到更高优先级的队列中,以此类推。如果一个进程完成了它的任务,它将被从队列中移除

这种调度算法的优点是能够很好地兼顾长作业和短作业,优先级较低的进程在等待队列中也会得到一定的时间片,从而避免了某些进程一直得不到服务的情况。同时,该算法也能够动态地调整进程的优先级,从而更加公平地分配CPU时间

然而,多级反馈队列调度算法也存在一些问题。例如,算法的效率不高,每个进程需要在多个队列之间移动,会造成大量的上下文切换。另外,该算法的实现比较复杂,需要考虑多个队列之间的调度问题。

#嵌入式##操作系统面试总结#

该专栏面向嵌入式开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等18篇文章。超全的嵌入式软件工程师笔试面试题目和高频知识点总结!招聘so easy。

全部评论

相关推荐

暮雨轻歌:看起来hr不能接受我菜查看图片
点赞 评论 收藏
分享
暴风雨来了:这不就是力扣的算法题吗?
点赞 评论 收藏
分享
评论
1
5
分享

创作者周榜

更多
牛客网
牛客企业服务