9 嵌入式软件面试 — 操作系统

9.1 linux系统

问题1:linux下如何编译程序?(

GCC编译器是Linux系统中最常用的编译器之一,它支持多种编程语言,包括C、C++、Fortran、Ada和Go等。基本的GCC编译命令格式如下:

gcc [options] source_file -o output_file
  • source_file 是你要编译的源代码文件。
  • output_file 是编译后生成的可执行文件名。
  • [options] 是一系列可选的编译选项。

[options]常见编译选项:

  • -c:只编译和汇编,但不链接。
  • -g:生成调试信息。
  • -Wall:打开大多数警告信息。
  • -O2:优化代码,提高执行效率。
  • -std=c99:指定使用C99标准。
  • -o output_file:指定输出文件名。

9.2 进程

问题2:进程的概念

进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。可以把进程想象成是正在运行的程序的实例,比如当你在电脑上同时打开多个软件,如浏览器、音乐播放器等,每个软件的运行状态就对应着一个进程。

Linux的进程结构,一般分为三部分:代码段、数据段(.data与.bss)和堆栈段。

  • 代码段:用于存放程序代码,如果有多个进程运行相同的一个程序,那么它们可以使用同一个代码段。代码段还会存储一部分常量,如字符串常量字面值。
  • 数据段:则存放程序的全局变量和静态变量。
  • 堆栈段:栈用于函数调用,存放着函数的参数、局部变量。

问题3:进程的5种状态

进程有五种状态:创建、就绪、执行、阻塞、终止。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。如图:

  • 创建状态:一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB: Process Control Block)完成资源分配。
  • 就绪状态:在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
  • 运行状态:获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
  • 阻塞状态:在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
  • 终止状态:进程结束或者被系统终止,进入终止状态

问题4:进程创建方式

进程的创建方式有两种:一种由操作系统创建;一种由父进程创建。

  1. 我们先讲由操作系统创建的进程。在系统启动时,操作系统会创建一些进程,它们承担着管理和分 配资源的任务,这些进程维持这系统的稳定运行,被称为系统进程。
  2. 另一种方式就是由父进程创建。系统允许一个进程创建新进程(即子进程),子进程又可以创建新 的子进程,形成树结构。子进程创建成功后,子进程将存在于系统之中,并且独立于父进程。子进 程可以接受系统调度,可以分配资源。

创建一个子进程,常用fork()函数,其原型如下:

#include <unistd.h>  
pid_t fork(void);  

/*
fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
(1)    对于父进程,fork()函数返回新创建的子进程的PID。
(2)    对于子进程,fork()函数调用成功会返回0。
(3)    如果创建出错,fork()函数返回-1。
*/

fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。

问题5:守护进程

守护进程是运行在操作系统后台的进程,它独立于任何终端,通常在系统启动时就被启动,并持续运行以提供特定的系统服务或执行特定的后台任务,直到系统关闭。

问题6:僵尸进程和孤儿进程

在Linux中,正常情况下,子进程是通过父进程创建的,子进程又创建新的进程,子进程退出后,将由父进程调用wait()或者waitpid()系统调用取得子进程的终止状态,然后就要回收子进程的资源。然而子进程的结束和父进程的运行是一个异步过程,即父进程无法预测子进程什么时候结束。于是就会产生孤儿进程和僵尸进程。

  • 孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。
  • 僵尸进程:是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
  • 两者区别:孤儿进程是父进程已退出,子进程未退出;而僵尸进程是父进程未退出,子进程已退出。
#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  

int main() {  
    pid_t pc = fork(); //创建一个子进程  
    if (pc > 0){//父进程  
        printf("in parent process, wait some minutes...\n");  
        sleep(10);  
        printf("after waiting, parent process exits!\n");  
    }  
    else if (pc == 0){  
        //子进程退出,父进程没有调用wait()或waitpid()系统调用  
        //子进程将成为一个僵尸进程  
        printf ("in child process, let it exist!\n");  
        exit(0);  
    }  
    return 0;  
}

问题7:wait()和waitpid()系统调用

wait函数主要用于使父进程阻塞,等待任意一个子进程终止,然后回收子进程资源并获取退出状态。它返回被回收子进程的进程 ID,若没有子进程或出错则返回 - 1。

waitpid函数与wait类似,用于等待子进程结束并回收资源。但它更灵活,可以指定等待的子进程。成功返回结束子进程的 ID,出错返回 - 1。

问题8:进程通讯管道

进程间通信就是在不同进程之间传递或交换信息。主要有三种方式:

  1. 管道。
  2. 系统IPC(消息队列,共享内存,信号量,信号)。
  3. 套接字。

fork创建的父子进程之间不共享数据段和堆栈段,它们之间是通过管道进行通信的。操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间同一时刻进行单向通信的机制。因为这种特性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指同一时刻数据只能由一个进程流向另一个进程;如果想实现全双工通信,需要建立两个管道。

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

  • 无名管道是一种半双工的通信机制,用于具有亲缘关系(如父子进程)之间的通信。它在内存中开辟一块缓冲区,数据从管道的一端写入,从另一端读出。管道的创建通过pipe系统调用实现,它会返回两个文件描述符,一个用于读,一个用于写。
  • 命名管道是一种特殊类型的文件,它有一个文件名,可以在文件系统中存在。不同的进程可以通过这个文件名来打开同一个命名管道进行通信,即使这些进程没有亲缘关系。命名管道通过mkfifo系统调用创建,进程可以使用open系统调用以读或写的方式打开命名管道。

问题9:进程通讯——系统IPC

系统IPC,有四种:消息队列,共享内存,信号量,信号。

  • 消息队列:消息队列这种通信机制传递的数据具有某种结构,而不是简单的字节流。消息队列的本质是内核提供的链表,内核基于这个链表实现了一个数据结构。向消息队列写数据,实际上是向这个数据结构中插入一个新节点;从消息队列读数据,实际上是从这个数据结构中删除一个节点。
  • 内存共享:共享内存允许多个进程访问同一块内存区域,从而实现数据的共享和交换。在共享内存中,多个进程可以将同一块物理内存映射到它们各自的虚拟地址中,使它们可以直接读写该内存的内容,而无需通过消息传递等其他通信方式。
  • 信号量:信号量的主要作用是控制多个进程对临界区的访问,以防止并发冲突。信号量是一种特殊的变量,它只能执行两种操作:P操作和V操作。P操作(要执行代码的时候)用于检查信号量的值,如果信号量的值大于0,则把该信号量减1并继续执行;如果信号量的值等于0,则挂起该进程。V操作(执行完代码的时候)用于恢复被挂起的进程(如果有的话)或把信号量加1。
  • 信号:信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号可以直接在用户空间进程和内核进程之间进行交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

问题10:进程通讯—socket套接字

socket套接字。前面我们讲的几种通信方式都是单机进程的通信。而socket套接字则可以实现多机通信。套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

9.3 线程

问题11:线程的概念

进程在早期的多任务操作系统中是基本的执行单元。一个进程就包含了程序指令和相关的资源集合,所有进程一起参与调度,竞争CPU、内存等系统资源。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。

一个进程里面可以包含多个线程,每个线程都可以执行一个任务。所有线程都可以共享进程的资源,如代码段、数据段。但是每个线程都有自己的局部变量,需要栈存储,所有每个线程也有自己独立的资源,如栈、寄存器。这样,线程之间的切换只需要切换独立的资源,是不是开销自然就比进程低了呀

问题12:线程与进程的区别

线程与进程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,进程包含一个或者多个线程。进程可以理解为完成一件事的完整解决方案,而线程可以理解为这个解决方案中的的一个步骤,可能这个解决方案就这只有一个步骤,也可能这个解决方案有多个步骤。

问题13:并发与并行

并发:是指两个或多个事件在同一时间间隔发生,并发是针对单核 CPU 提出的,在同一CPU上的多个事件。

并行:是指两个或者多个事件在同一时刻发生,并行则是针对多核 CPU 提出,在不同CPU上的多个事件

问题14:什么是多线程(

多线程是实现并发(并行)的手段,并发(并行)即多个线程同时执行,一般而言,多线程就是把执行一件事情的完整步骤拆分为多个子步骤,然后使得这多个步骤同时执行。

问题15:线程通讯——互斥锁

线程通信是指在同一个进程中不同线程之间进行数据交换的过程。由于线程共享同一进程的内存空间,因此线程间的通信相对进程间通信更为简单,但也需要注意同步和互斥的问题。以下是线程通信中常用的几种同步机制:

互斥锁(Mutex):互斥锁用于保护临界区,确保一次只有一个线程可以访问共享资源,互斥锁只有两种状态,即上锁(lock)和解锁(unlock)。

  • 优点:简单易用,保证了共享资源的互斥访问。
  • 缺点:如果不当使用,可能会导致死锁。

问题16:线程通讯——信号量

信号量(Semaphore):信号量是一个计数器,用于控制多个线程对共享资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于等于0时,则可以访问,否则将阻塞。PV原语是对信号量的操作,一次P操作使信号量减1,一次V操作使信号量加1。

  • 优点:不仅可以用于互斥,还可以用于同步,允许多个线程同时访问资源。
  • 缺点:使用不当可能会导致资源使用不当或死锁。

问题17:线程通讯——条件变量

条件变量。与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

问题18:线程通讯——读写锁

读写锁。读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高,读写锁适合于对数据的读次数比写次数多得多的情况。

读写锁有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态。写锁优先级高于读锁。一次只有一个线程可以对其加锁,不论是加读锁还是加写锁。

问题19: 线程池

线程池(Thread Pool)是一种多线程管理技术,它通过预先创建一组线程来执行任务,而不是每次任务到来时都创建和销毁线程。线程池的主要目的是减少线程创建和销毁的开销,提高系统资源的利用率,并且可以控制同时运行的线程数量,避免系统因创建过多线程而导致的资源耗尽。实现线程池有以下几个步骤: (1)设置一个生产者消费者队列,作为临界资源。 (2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行 (3)当任务队列为空时,所有线程阻塞。 (4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。

9.4 协程

问题20:什么是协程

协程(Coroutine)是一种程序组件,它允许挂起和恢复执行,与线程相比,协程是轻量级的,它们主要在用户态管理,而不是由操作系统内核管理。这意味着协程的创建、销毁、切换开销远小于线程。协程通常用于提高程序的并发性能,尤其是在I/O密集型和高级别结构化异步编程中。

9.5 虚拟内存

问题21:虚拟内存概念

虚拟内存是一种内存管理技术,虚拟内存技术使得不同的进程在运行过程中,它所看到的是自己独占的4G内存空间。所有进程共享同一物理内存,每个进程只把自己的虚拟内存空间映射并存储到物理内存上。如图可以直观看到:

两个进程P1和P2都有自己的虚拟内存空间,但是最后都会映射到物理内存空间,只是P1和P2自己不知道而已,它们以为自己就是拥有独立内存空间。

(1)扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。 (2)内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。 (3)可以实现内存共享,方便进程通信。 (4)可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。

虚拟内存的代价:

(1)虚拟内存需要额外构建数据结构,占用空间。 (2)虚拟地址到物理地址的转换,增加了执行时间。 (3)页面换入换出耗时。 (4)一页如果只有一部分数据,浪费内存。

问题22:分页和页表

分页和页表:虚拟内存是操作系统里的概念,对操作系统来说,虚拟内存就是一张张的对照表,P1 获取 A 内存里的数据时应该去物理内存的 A 地址找,而找 B 内存里的数据应该去物理内存的 C 地址。

我们知道系统里的基本单位都是 Byte 字节,如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 4字节(32位虚拟地址->32位物理地址),在 4G 内存的情况下,就需要 16GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了页(Page)的概念。

在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了,对于一个32位的操作系统,其虚拟地址空间最大为2^32字节,即4GB。如果每页的大小为4KB(即4*2^10字节),那么整个虚拟地址空间需要的页表大小为(2^32 / (4 *2^10)) * 4字节 = 4MB,只需要 4M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。操作系统虚拟内存到物理内存的映射表,就被称为页表。

问题23:内存寻址和分配

(2)内存寻址和分配:我们知道通过虚拟内存机制,每个进程都以为自己占用了全部内存,进程访问内存时,操作系统都会把进程提供的虚拟内存地址转换为物理地址,再去对应的物理地址上获取数据。 CPU 中有一种硬件,内存管理单元 MMU(Memory Management Unit)专门用来将翻译虚拟内存地址。CPU 还为页表寻址设置了缓存策略,由于程序的局部性,其缓存命中率能达到 98%。 以上情况是页表内存在虚拟地址到物理地址的映射,而如果进程访问的物理地址还没有被分配,系统则会产生一个缺页中断,在中断处理时,系统切到内核态为进程虚拟地址分配物理地址。 我们以三级页表机制介绍,对于大内存需求,操作系统甚至可以四、五级页表寻址,但是原理和三级页表寻址机制是一样的,接下来我们具体介绍。 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表。页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。

问题24:三级页表转换方法

(1)逻辑地址转线性地址:段起始地址+段内偏移地址=线性地址 (2)线性地址转物理地址:

每一个32位的线性地址被划分为三部分:页目录索引(10位)、页表索引(10位)、页内偏移(12位)

  • 从cr3中取出进程的页目录地址(操作系统调用进程时,这个地址被装入寄存器中)
  • 页目录地址 + 页目录索引 = 页表地址
  • 页表地址 + 页表索引 = 页地址
  • 页地址 + 页内偏移 = 物理地址

问题25:缺页置换算法

缺页当然涉及到缺页置换算法,缺页异常发生后将产生一个缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。但是,此时内存已没有空闲空间,就需要从内存中调出一页程序或数据,送入到磁盘的对换区。这个选择调出页面的方法就叫缺页置换算法:

  • 先进先出(First In First Out, FIFO)算法:队列,删除队首的页即可
  • 最近最久未使用置换(Least Recently Used,LRU)算法:置换最近一段时间以来最长时间未访问的页面。
  • 最近最少使用(Least Frequently Used,LFU)算法:置换最近一段时间以来访问频率最低的页面。
  • LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉。实现方式是利用链表和哈希表。具体的做法是:当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。 在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

9.6 锁

问题26:锁的概念

所谓的锁,其实就是一个变量,拥有两种状态:1表示空闲状态,0表示上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。

加锁过程可以表示为:(1)读锁。(2)判断锁状态。(3)如果已加锁,失败返回。(4)把锁设置为加锁状态。(5)返回成功。

虽然每一步是原子性的,但是每一步之间却是可以中断的。比如进程A在执行完2后发生中断,中断中进程B也执行了加锁过程,返回中断后就会发生两个进程都会加锁。对于这个问题,计算机已经解决,方法是采用原子级汇编指令test and set 和swap。

问题27:锁的分类

(1)互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。 (2)读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁。 (3)自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。 (4)条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用) (5)信号量。

互斥锁和读写锁的区别: (a)读写锁区分读锁和写锁,而互斥锁不区分 (b)互斥锁同一时间只允许一个线程访问,无论读写;读写锁同一时间只允许一个线程写,但可以多个线程同时读。

问题28:什么是死锁

死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。此时称系统处于死锁状态或系统产生了死锁。这些永远在互相等待的进程称为死锁进程。比如两只羊过独木桥。进程比作羊,资源比作桥。若两只羊互不相让,争着过桥,就产生死锁。

死锁产生的主要原因有:(1)系统资源不足。(2)进程运行推进的顺序不合适。(3)资源分配不当等。

产生死锁的必要条件:

(1)互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到占有该资源的进程使用完成后释放该资源; (2)请求保持条件:进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源; (3)不可剥夺条件:进程已获得的资源,只能自己释放,不可剥夺; (4)环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。存在一个进程等待序列{P1,P2,… Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一 源,……,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。

解决办法:

(1)资源一次性分配,从而解决请求保持的问题 (2)可剥夺资源:当进程新的资源未得到满足时,释放已有的资源; (3)资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。

9.7 操作系统资源调度方法

问题29:操作系统的资源调度方法

这就涉及到操作系统的调度问题了。有几种调度方法: (1)先来先服务调度算法:每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的作业(进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。 (2)短作业(进程)优先调度算法:短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业(进程),将它们调入内存运行。 (3)高优先级优先调度算法:当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。 (4)时间片轮转法:每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。 (5)多级反馈队列调度算法:综合前面多种调度算法。

问题30:抢占式和非抢占式调度算法的区别

(1)非抢占式优先权算法 在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。 (2)抢占式优先权调度算法 在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

  • 非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。
  • 抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)

9.8 IO模型的类型

问题31:IO模型的类型

IO模型的类型。IO(Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作。通常用户进程中的一个完整IO分为两阶段:用户进程空间与内核空间之间的相互切换、内核空间与设备空间的相互切换(磁盘、网络等)。我们通常说的IO是指网络IO和磁盘IO两种。 Linux中进程无法直接操作I/O设备,其必须通过系统调用请求内核来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。 对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。 所以,对于一个网络输入操作通常包括两个不同阶段:

  1. 等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
  2. 从内核缓冲区复制数据到进程空间。

5种IO模型如下:

(1)阻塞IO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。调用者将一直等待,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。

(2)非阻塞IO:进程发起IO系统调用后,进程被阻塞,内核数据还没好,不想让进程等待,就返回一个错误,这样进程就不阻塞了。进程每隔一段时间就发起IO系统调用去检查IO事件是否就绪。这样就实现非阻塞了。每个进程都有一个时间片,轮询的时候读取IO,时间片到了就要换另一个进程做其他事情了,这样就做到了每隔一段时间发起IO系统调用。

(3)IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。select/poll会监听所有的IO,直到有数据可读或可写时,才真正调用IO操作函数。

(4)信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收 到SIGIO信号,然后处理IO事件。这个好理解,这个信号直接通知进程数据到了。

(5)异步IO:进程发起IO系统调用后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。具体操作是进程调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回。

问题32:同步与异步的区别

同步:所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。双方的动作是经过双方协调的,步调一致的。

异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。双方并不需要协调,都可以随意进行各自的操作。

问题33:阻塞与非阻塞的区别

阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。 非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。

嵌入式软件面试宝典 文章被收录于专栏

嵌入式软件面试宝典包含简历制作、笔试准备、面试八股文、企业真题等。

全部评论

相关推荐

不愿透露姓名的神秘牛友
10-31 20:53
点赞 评论 收藏
分享
6 15 评论
分享
牛客网
牛客企业服务