Live2D

【操作系统】面试题总结(持更)

进程

进程就是程序的一次执行过程,程序是静态的,它作为系统中的一种资源是永远存在的。而进程是动态的,它是动态的产生,变化和消亡的,拥有其自己的生命周期。

进程的组成

  • 进程控制块PCB,

    • 1)进程描述信息:用来让操作系统区分各个进程

      • 当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的 “身份证号”— PID(ProcessID,进程 ID)
      • 另外,进程描述信息还包含进程所属的用户 ID(UID
    • 2)进程控制和管理信息:记录进程的运行情况。比如 CPU 的使用时间、磁盘使用情况、网络流量使用情况等。

    • 3)资源分配清单:记录给进程分配了哪些资源。比如分配了多少内存、正在使用哪些 I/O 设备、正在使用哪些文件等。

    • 4)CPU 相关信息:进程在让出 CPU 时,必须保存该进程在 CPU 中的各种信息,比如各种寄存器的值。用于实现进程切换,确保这个进程再次运行的时候恢复 CPU 现场,从断点处继续执行。这就是所谓的保存现场信息

  • 数据段。即进程运行过程中各种数据(比如程序中定义的变量)

  • 程序段。就是程序的代码(指令序列)

进程的状态

经典的进程三态模型如下:

  • 运行态(running):进程占有 CPU 正在运行。

  • 就绪态(ready):进程具备运行条件,等待系统分配 CPU 以便运行。

  • 阻塞态 / 等待态(wait):进程不具备运行条件,正在等待某个事件的完成。

    img

需要注意的是:阻塞态是由于缺少需要的资源从而由运行态转换而来,但是该资源不包括 CPU 时间片,缺少 CPU 时间片会从运行态转换为就绪态

很多系统中都增加了新建态(new)和终止态(exit),形成五态模型

  • 新建态(new):进程正在被创建时的状态
  • 终止态(exit):进程正在从系统中消失时的状态

img

从上图可以发现,只有就绪态和运行态可以相互转换,其它的都是单向转换,阻塞状态结束进入就绪态而不可能直接进入运行态。

进程的阻塞和唤醒显然是由进程切换来完成的。

进程阻塞和唤醒步骤

进程的阻塞步骤,也就是阻塞原语的内容为:

  • 找到将要被阻塞的进程对应的 PCB;
  • 保护进程运行现场,将 PCB 状态信息设置为阻塞态,暂时停止进程运行;
  • 将该 PCB 插入相应事件的阻塞队列(等待队列)。

进程的唤醒步骤,也就是唤醒原语的内容为:

  • 在该事件的阻塞队列中找到相应进程的 PCB;
  • 将该 PCB 从阻塞队列中移出,并将进程的状态设置为就绪态;
  • 把该 PCB 插入到就绪队列中,等待被调度程序调度。

阻塞原语和唤醒原语的作用正好相反,阻塞原语使得进程从运行态转为阻塞态,而唤醒原语使得进程从阻塞态转为就绪态。如果某个进程使用阻塞原语来阻塞自己,那么他就必须使用唤醒原语来唤醒自己,因何事阻塞,就由何事唤醒,否则被阻塞的进程将永远处于阻塞态。因此,阻塞原语和唤醒原语是成对出现的

进程通信

为什么需要进程间通信?

1).数据传输:一个进程需要将它的数据发送给另一个进程;

2).资源共享:多个进程之间共享同样的资源;(可能会从死锁那里引申过来)

3).通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件;

4).进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),该控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。

进程间通信原理

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。

主要的过程如下图所示:

img
(本质都是在内核开辟一块空间共享)

进程间通信方式

管道(内存中的特殊文件)、命名管道(硬盘上的文件)、消息队列(内核)、共享存储、信号量、套接字、信号
详细可参考:https://www.jianshu.com/p/c1015f5ffa74

  • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。进程间通信的方式是匿名的,所以只能用于具有亲缘关系的进程间通信

    • 最基本的IPC机制,由pipe函数创建,

    • 管道在用户程序看起来就像一个打开的文件,通过read(pipefd[0])或者write(pipefd[1])向这个文件读写数据,其实是在读写内核缓冲区

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

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

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

    • 管道出现的四种特殊情况:

      1.写端关闭,读端不关闭;

      那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

      2.写端不关闭,但是也不写数据,读端不关闭;

      此时管道中剩余的数据都被读取之后再次read会被阻塞,直到管道中有数据可读了才重新读取数据并返回;

      3.读端关闭,写端不关闭;

      此时该进程会收到信号SIGPIPE,通常会导致进程异常终止。

      4.读端不关闭,但是也不读取数据,写端不关闭;

      此时当写端被写满之后再次write会阻塞,直到管道中有空位置了才会写入数据并重新返回。

    • 使用管道的缺点

      1.两个进程通过一个管道只能实现单向通信,如果想双向通信必须再重新创建一个管道或者使用sockpair才可以解决这类问题;

      socketpair()函数用于创建一对无名的、相互连接的套接子。
      如果函数成功,则返回0,创建好的套接字分别是sv[0]sv[1];否则返回-1,错误码保存于errno中。

      基本用法:

      1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
      2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
      3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。

      2.只能用于具有亲缘关系的进程间通信,例如父子,兄弟进程。

      3.只能承载无格式字节流以及缓冲区大小受限

  • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。命名管道的名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。

    • FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储文件系统中。命名管道是一个设备文件,因此即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。

    • 命名管道的特点:

      1.命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。所以当使用命名管道的时候必须先open将其打开。

      2.命名管道可以用于任何两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。

  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点

    • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
    • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
    • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。
    • 消息队列特点总结:
      -(1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
      -(2)消息队列允许一个或多个进程向它写入与读取消息.
      -(3)管道和消息队列的通信数据都是先进先出的原则。
      -(4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
      -(5)消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
      -(6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。
  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

    • 共享内存是这五种进程间通信方式中效率最高的。但是因为共享内存没有提供相应的互斥机制,所以一般共享内存都和信号量配合起来使用。

    img

    • 消息队列,FIFO,管道的消息传递方式一般为 :

      1).服务器获取输入的信息;

      2).通过管道,消息队列等写入数据至内存中,通常需要将该数据拷贝到内核中;

      3).客户从内核中将数据拷贝到自己的客户端进程中;

      4).然后再从进程中拷贝到输出文件;

    • 上述过程通常要经过4次拷贝,才能完成文件的传递。而共享内存只需要:

      1).输入内容到共享内存区

      2).从共享内存输出到文件

    • 上述过程不涉及到内核的拷贝,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间,所以共享内存是这五种进程间通信方式中效率最高的。

    • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。

  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

    信号量的本质是一种数据操作锁,用来负责数据操作过程中的互斥,同步等功能。

    信号量用来管理临界资源的。它本身只是一种外部资源的标识,不具有数据交换功能,而是通过控制其他的通信资源实现进程间通信。 可以这样理解,信号量就相当于是一个计数器。当有进程对它所管理的资源进行请求时,进程先要读取信号量的值:大于0,资源可以请求;等于0,资源不可以用,这时进程会进入睡眠状态直至资源可用。

    当一个进程不再使用资源时,信号量+1(对应的操作称为V操作),反之当有进程使用资源时,信号量-1(对应的操作为P操作)。对信号量的值操作均为原子操作。

  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

    • 凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
    • 套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
    • 套接字建立:

    image

    • 服务器端
      (1)首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
      (2)然后,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
      (3)接下来,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
      (4)最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。

    • 客户端
      (1)客户应用程序首先调用socket来创建一个未命名的套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
      (2)一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)。

  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

    • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
    • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
    • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

    信号生命周期和处理流程
    (1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
    (2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
    (3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
    image

线程与进程对比

进程(process)是指在系统中正在运行的一个应用程序,是系统资源分配的基本单位,在内存中有其完备的数据空间和代码空间,拥有完整的虚拟空间地址。一个进程所拥有的数据和变量只属于它自己。

线程(thread)是进程内相对独立的可执行单元,所以也被称为轻量进程(lightweight processes );是操作系统进行任务调度的基本单元。它与父进程的其它线程共享该进程所拥有的全部代码空间和全局变量,但拥有独立的堆栈(即局部变量对于线程来说是私有的)

线程和进程的区别

  1. 线程是进程的一部分,所以线程有的时候被称为是轻量级进程。
  2. 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程, 进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
  3. 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使 用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运 行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属 进程的资源
  4. 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表 中少很多
  5. 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线 程而独立存在。

线程的优缺点

优点

  • 资源开销小。创建另一个线程的内存消耗量被限制为线程所需要的堆栈加上线程管理器需要的一些簿记内存。
  • 线程之间共享资源方便。不需要采用先进的技术来访问服务器全局数据。如果数据有可能由另一个同时运行的线程修改, 则要做的一切就是使用互斥体 保护相关段。
  • 创建线程所需要的时间大大小于创建进程所需要的时间,原因是不必复制堆部分(它可能很大)
  • 响应速度快。在线程之间进行环境切换时,内核在调度器中花的时间比在过程之间进行切换花的时间少。 这给负担很重的服务器留下了更多的cpu时间处理工作。切换速度快,因此线程响应时间也更快。
  • 便于操作系统调度:由于线程的创建和销毁比进程快,因此操作系统可以更方便地调度和管理线程,提高系统的效率。

缺点
尽管线程在现代计算机中极具重要性,它们却有很多缺点:

  • 容易产生编程错误。程序员必须不断考虑其他一些线程可能正在做引起麻烦的事情,以及如何避免这种麻烦。需要采用额外的防范方法编制程序。(拓展->什么方法?线程锁)
  • 容易出现竞态条件:由于多个线程可以并发地访问共享资源,因此容易出现数据竞争和竞态条件,导致程序出现不可预测的错误。
  • 线程间通信复杂:由于线程之间共享资源,因此必须进行适当的同步和通信,以确保数据的正确性和一致性。线程间通信需要使用锁、条件变量等同步机制,代码复杂度较高,容易出错。(注意上面优点中是:共享资源方便,但是通信复杂)
  • 编程错误代价惨重。如果一个线程崩溃,会使得整个服务器停止。一个坏线程可能会毁坏全局数据,导致其他线程无法工作。
  • 可能会影响性能:线程的创建和销毁比进程快,但线程之间的切换仍然需要时间和系统资源。如果线程数过多,会导致系统资源的消耗和调度负担增加,反而降低了系统的性能。
  • 调试困难。线程服务器在同步漏洞方面声名狼藉,这些漏洞几乎无法在测试中进行复制,却在生产期间很不合时宜地出现。这类漏洞发生几率之所以如此高,是由于使用共享地址空间,这会产生更高程度的线程交互。
  • 可能会影响性能。有时候互斥体争用难以控制。如果在同一时间有太多的线程想得到相同的互斥体,就会导致过度的环境切换,大量的CPU时间就会花在内核调度器上,因而几乎没有时间执行工作。
  • 32位系统限制为每个线程使用4G地址空间。由于所有的线程都共享相同的地址空间,理论上整 个服务器就被限制为使用4G RAM,即便实际上有更多的物理RAM也不例外。实际使用时,地址空间 会在一个小得多的限值下开始变得非常拥挤。
  • 拥挤的32位地址空间会带来另一个问题。每个线程都需要一些堆栈空间,在分配了堆栈后, 即便不使用所分配的大部分空间,服务器也会为其保留地址空间。每个新堆栈都会减少用于堆的 潜在空间。因此,即使有足够的物理内存,也不可能同时使用大型缓冲区,同时使用大量并发线 程,以及同时为每个线程提供足够的堆栈空间。

进程的优缺点

优点

  • 编程错误并不致命。尽管有可能发生,但一个坏分支服务器进程并不容易中断整个服务器。
  • 编程错误发生的可能性小得多。在大多数时候,程序员只需要考虑一个线程的执行,而不用受可能的并发侵入者的打扰。
  • 飘忽不定的漏洞少得多。如果漏洞出现一次,通常非常容易复制。由于各个分支进程有自己的 地址空间,它们之间并没有太多的交互。

缺点

  • 内存利用不够好。当子进程发生分支时,会不必要地复制大型内存程序段。
  • 需要采用特殊技术实现进程数据共享。(IPC:InterProcess Communication进程间通信)
  • 创建进程比创建线程需要更多的内核系统开销。对性能的一个很大的打击是需要 复制父进程的数据段。不过,Linux 在这方面的手段是执行所谓的copy-on-write 。除非子进程或父进程修改了父进程页,否则并不真正复制父进程页。在此之前,父子进程使用相同的页。
  • 进程之间的环境切换比线程之间的环境切换更为耗时,因为内核需要切换页,文件描述符表及其他额外的内容信息。留给服务器执行实际工作的时间减少。

进程与线程锁机制

线程锁

线程锁:大家都不陌生,主要用来给方法、代码块加锁。当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。

  1. 什么是线程锁
  • 多线程可以同时运行多个任务
  • 但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!so,不使用线程锁, 可能导致错误

啰嗦两句: 比如你在银行取钱的同时你女朋友用支付宝取钱 不同线程同时访问同一资源 如果资源不加锁可能会导致银行亏本 卡里有100却取出200这种错误
在计算机编程中,线程锁是一种同步原语,用于控制多个线程对共享资源的访问。

下面是几种常见的线程锁方式

  1. 互斥锁(Mutex):也称为互斥量,是一种最常见的线程锁方式,用于确保同一时刻只有一个线程访问共享资源。当一个线程获取到互斥锁后,其他线程就无法获取到该锁,只能等待该线程释放锁。
  2. 读写锁(ReadWrite Lock):也称为共享-独占锁,允许多个线程同时读取共享资源,但只有一个线程能够独占地写入共享资源。在读取共享资源时,多个线程可以同时获取读锁,而在写入共享资源时,只有一个线程可以获取写锁。
  3. 自旋锁(Spin Lock):是一种忙等待的锁,当一个线程尝试获取锁时,如果锁已被其他线程持有,则该线程会不断地进行自旋等待,直到锁被释放。自旋锁适用于锁被占用的时间非常短的情况。只要没有锁上,就不断重试。
  4. 条件变量(Condition Variable):条件变量是一种用于线程间通信的同步机制,它允许一个或多个线程等待某个特定条件变为真时再继续执行。通常,条件变量与互斥锁一起使用,以避免竞态条件(Race Condition)的出现。
  5. 信号量(Semaphore):是一种用于多线程编程中的同步原语,用于控制多个线程对共享资源的访问。信号量可以用来实现多个线程的互斥访问和同步执行,也可以用来实现资源池和任务队列等高级应用。

进程锁

进程锁:也是为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制(操作系统基本知识)。

在操作系统中,进程锁是为了避免多个进程同时对共享资源进行读写操作而引起的数据竞争问题,常常需要采用的一种锁机制。常见的进程锁包括:

  1. 互斥锁(Mutex):与线程锁类似,互斥锁也是最基本的一种锁,只有当锁被释放后,其他进程才能获取到锁。通常使用操作系统提供的互斥锁实现。
  2. 读写锁(ReadWrite Lock):与线程锁类似,读写锁也分为读锁和写锁两种,多个进程可以同时获取读锁,但只有一个进程可以获取写锁。当读写锁处于写模式时,任何尝试获取读锁或写锁的进程都必须等待。读写锁适用于读多写少的场景。
  3. 信号量(Semaphore):信号量是一种计数器,用于控制多个进程对共享资源的访问。当信号量为正时,表示可用资源的数量;当信号量为零时,表示资源已经被占用。进程可以通过信号量来请求或释放资源。
  4. 屏障(Barrier):屏障是一种同步机制,用于协调多个进程在某个时间点上的执行顺序。当所有进程都达到屏障点时,才能一起继续执行。

除了以上常见的进程锁外,还有一些高级的锁机制,如读写者锁、分段锁等,不同的锁机制适用于不同的并发场景。

posted @ 2023-03-23 14:41  WSquareJ  阅读(65)  评论(0编辑  收藏  举报