操作系统----校招笔试面试常考内容总结(待续)

1. 进程的五种基本状态

(1) 五状态模型
创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
执行状态:进程处于就绪状态被调度后,进程进入执行状态
阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
在这里插入图片描述
(2)上图显示了导致进程状态转换的事件类型,可能的转换如下:
空->新建:创建执行一个程序的新进程,可能的事件有:新的批处理作业、交互登录(终端用户登录到系统)、操作系统因为提供一项服务而创建、由现有的进程派生等。

新建->就绪:操作系统准备好再接纳一个进程时,把一个进程从新建态转换为就绪态。

就绪->运行:需要选择一个新进程运行时,操作系统的调度器或分配器根据某种调度算法选择一个处于就绪态的进程。

运行->退出:导致进程终止的原因有:正常完成、超过时限、系统无法满足进程需要的内存空间、进程试图访问不允许访问的内存单元(越界)、算术错误(如除以0或存储大于硬件可以接纳的数字)、父进程终止(操作系统可能会自动终止该进程所有的后代进程)、父进程请求终止后代进程等。

运行->就绪:最常见的原因是,正在运行的进程到达了“允许不中断执行”的最大时间段,该把处理器的资源释放给其他在就绪态的进程使用了;还有一中原因可能是由于具有更改优先级的就绪态进程抢占了该进程的资源,使其被中断转换到就绪态。

运行->阻塞:如果进程请求它必须等待的某些事件,例如一个无法立即得到的资源(如I/O操作),只有在获得等待的资源后才能继续进程的执行,则进入等待态(阻塞态)。

阻塞->就绪:当等待的事件发生时,处于阻塞态的进程转换到就绪态。

就绪->退出:在上图中没有标出这种转换,在某些进程中,父进程可以在任何时刻终止一个子进程,如果一个父进程终止,所有相关的子进程都被终止。

阻塞->退出:跟上一项原因类似。

2. 进程与线程的区别

一个进程被定义成资源分配的单位和一个被保护的单位,与进程相关联的有:
(1)存放进程映像(程序、数据、栈和进程控制块中定义的属性的集合)的虚拟地址空间
(2)受保护的对处理器、其他进程(用于进程间通信)、文件和IO资源(设备和通道)的访问

在一个进程中,可能有一个或多个线程,每个线程有:
(1)线程执行状态(运行、就绪等)
(2)在未运行时保存的线程上下文
(3)一个独立的执行栈
(4)用于每个线程局部变量的静态存储空间
(5)与进程内的其他线程共享的对进程的内存和资源的访问
(6)独立的线程控制块用于包含寄存器值、优先级和其他与线程相关的状态信息。

在大多数操作系统中,独立进程间的通信需要内核的介入,以提供保护和通信所需要的机制。但是,由于在同一个进程中的线程共享内存和文件,他们无需调用内核就可以相互通信。

3.进程通信的几种方式

进程间通信主要包括管道, 系统IPC(包括消息队列,信号量,共享存储), SOCKET。

  • 1. 管道

    管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
    管道包括三种
    1)普通管道PIPE, 通常有种限制,一是半双工,只能单向传输;二是只能在父子进程间使用.
    2)流管道s_pipe: 去除了第一种限制,可以双向传输.
    3)命名管道:name_pipe,去除了第二种限制,可以在许多并不相关的进程之间进行通讯.
    有名管道 (namedpipe)
    有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 2. 系统IPC

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

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

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

    共享内存( sharedmemory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC
    方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

  • 3. 套接字

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

4. 线程同步几种方式

(一定要会写生产者、消费者问题,完全消化理解)
进程中线程同步的四种常用方式:

  • (1)临界区(CriticalSection)
    当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。具体应用方式:
    (1、 定义临界区对象
    (2、 在访问共享资源(代码或变量)之前,先获得临界区对象
    (3、 访问共享资源后,则放弃临界区对象

  • (2)事件(Event)
    事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。比如在某些网络应用程序中,一个线程如A负责侦听通信端口,另外一个线程B负责更新用户数据,利用事件机制,则线程A可以通知线程B何时更新用户数据。

- (3)互斥量(Mutex)
互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。

(4)信号量(Semphore)
当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。Semaphore类对象保存了对当前访问某一个指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程数目。如果这个计数达到了零,则所有对这个Semaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。

生产者-消费者模型:

要理解生产消费者问题,首先应弄清PV操作的含义:
PV操作是由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:
P(S)
①将信号量S的值减1,即S=S-1;
②如果S³0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
V(S)
①将信号量S的值加1,即S=S+1;
②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

P操作相当于申请资源,而V操作相当于释放资源。

生产者-消费者问题是一个有代表性的进程同步问题,生产者-消费者问题,也称作有界缓冲区问题,两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,或者当缓冲区空时,消费者还要从中取出数据项的问题。为了保证这种情况不会发生,我们通常使用信号量和消息传递来解决生产者-消费者问题。

(1)使用信号量解决生产者-消费者问题

一个信号量的取值可以为 0(表示没有保存下来的唤醒操作)或者为正值(表示有一个或多个唤醒操作)。
并且设立了两种操作:
down和 up(也是一般教科书上说的 P/V向量)。
对一个信号量执行 down操作,表示检查其值是否大于 0,如果该值大于 0,则将其值减 1(即用掉一个保存的唤醒信号)并继续;如果为 0,则进程休眠,而且此时 down操作并未结束。另外,就是检查数值,修改变量值以及可能发生的休眠操作都作为单一的,不可分割的原子操作来完成。

下面开始考虑用 信号量 来解决生产者-消费者问题了。

#define N 100                           // 缓冲区中的槽数目
typedef int semaphore;               // 信号量一般被定义为特殊的整型数据
semaphore mutex = 1;               // 控制对临界区的访问
semaphore empty = N;               // 计数缓冲区中的空槽数目
semaphore full = 0;                 // 计数缓冲区中的满槽数目
/* 生产者进程 */
void proceducer(void)
{
        int item;
        while(1)
        {
               item = procedure_item();       // 生成数据
               down(&empty);                              // 将空槽数目减 1
               down(&mutex);                              // 进入临界区
               insert_item(item);                       // 将新数据放入缓冲区
               up(&mutex);                                      // 离开临界区
               up(&full);                                      // 将满槽的数目加 1
        }
}
/* 消费者进程 */
void consumer(voi)
{
        int item;
        while(1)
        {
               down(&full);                              // 将满槽数目减 1
               down(&mutex);                              // 进入临界区
               item = remove_item();               // 从缓冲区中取出数据项
               up(&mutex);                                      // 离开临界区
               up(&empty);                                      // 将空槽数目加 1
               consumer_item(item);               // 处理数据项
        }
}

该解决方案使用了三个信号量
一个为 full,用来记录充满的缓冲槽的数目。
一个为 empty,记录空的缓冲槽总数。
一个为 mutex,用来确保生产者和消费者不会同时访问缓冲区。mutex的初始值为 1,供两个或者多个进程使用的信号量,保证同一个时刻只有一个进程可以进入临界区,称为二元信号量(binary semaphore)。如果每一个进程在进入临界区前都执行一个 down(…),在刚刚退出临界区时执行一个 up(…),就能够实现互斥。

另外,通常是将 down和 up操作作为系统调用来实现,而且 OS只需要在执行以下操作时暂时禁止全部中断:测试信号量,更新信号量以及在需要时使某个进程休眠。

这里使用了三个信号量,但是它们的目的却不相同,其中 full和 empty用来同步(synchronization),而 mutex用来实现互斥。

(2)使用消息传递解决生产者-消费者问题

这种 IPC方式使用两条原语 send和 receive,也是系统调用。
如:
send(dest, &msg) // 将消息 msg发送到目标(进程)dest中
receive(src, &msg) // 接收由 src过来的 msg,如果没有消息可用,则可能阻塞接收者

消息传递系统会面临位于网络中不同机器上的通信进程的情形,所以会更加的复杂。如:消息可能被网络丢失,一般使用确认(ACK)消息。如果发送方在一定的时间段内没有收到确认消息,则重发消息。

如果消息本身被正确接收,但是返回的 ACK消息丢失,发送方则重发消息,这样接收方就会收到两份同样的消息。一般使用在每条原始消息的头部嵌入一个连续的序号来解决这个问题。

另外,消息传递系统还需要解决进程命名的问题,在 send和 receive系统调用中指定的进程必须没有二义性的。还有其他的一些问题,如性能问题,身份认证等等,不过那个就会扯多了,还是看看如果解决这个生产者-消费者的问题吧:

#define N 100                              // 缓冲区中的槽数目
/* 生产者进程 */
void proceducer(void)
{
        int item;
        messagemsg;                       // 消息缓冲区
        while(1)
        {
               item = procedure_item();       // 生成数据
               receive(consumer, &msg);       // 等待消费者发送空的缓冲区
               build_msg(&msg, item);               // 创建待发送消息
               send(consumer, &msg);               // 发送数据项给消费者
        }
}
/* 消费者进程 */
void consumer(voi)
{
        int item,i;
        messagemsg;
        for(i=0;i<N; i++)
               send(producer, &msg);               // 发送给生产者 N 个空缓冲区
   
        while(1)
        {
               receive(producer, &msg);       // 接收包含数据项的消息
               item = extract_item(&msg);       // 解析消息,并组装成数据项
               send(proceduer, &msg);               // 然后又将空缓冲区发送回生产者
               consumer_item(item);               // 处理数据项
        }
}

在这个解决方案中,共使用了 N条消息,有点类似于上一个的共享内存缓冲区的 N个槽,消费者进程这边首先通过一个 for循环将 N条空消息发送给生产者。当生产者向消费者传递一个数据项时,是通过取走每一条接收到的空消息,然后送回填充了内容的消息给消费者的。通过这种方式,整个消息传递系统中的总的消息数(包括空的消息 + 存了数据项的消息 == N)是不变的。

如果运行过程中,生产者进程的速度比消费者快,则所有的消息最终都会塞满,然后生产者进程就会等待消费者(即使调用 procedure也是阻塞在 receive处),直到消费者返回一条空的消息;反之亦然。

下面再来看一下消息传递方式的两种变体

  • 一种是:为每一个进程分配一个唯一的地址,让消息按照这个进程的地址进行编址。也就是 send和
    receive调用的第一个参数指定为具体的进程地址。
  • 另一种是:引入信箱(mailbox),可以信箱就像一个盒子,里面装了很多的信件,这个信件就是我们要传递的消息,当然信箱是有容量限制的。当使用信箱时,send和
    receive系统调用中的地址参数就是信箱的地址,而不是进程的地址。当一个进程尝试向一个容量爆满的信箱发送消息时,它将会被挂起,直到信箱中有消息被取走。

5. 线程的实现方式.

(也就是用户线程与内核线程的区别)

线程的实现可分为两大类
用户级线程(user-levelthread,ULT)和内核级线程(kernel-levelthread,KLT)。后者又称为内核支持的线程或轻量级进程。

(1)用户级线程

在一个纯粹的用户级线程软件中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。
使用用户级线程的优点有

  • 线程切换不需要内核态特权,进程并不需要为了线程管理而切换到内核态
  • 可以为应用程序量身定做调度算法而不扰乱底层的操作系统调度程序
  • 用户级线程可以在任何操作系统中运行,不需要对底层内核进行修改以支持用户级线程

用户级线程有两个明显的缺点

  • 许多系统调用都会引起阻塞,当用户级线程执行一个系统调用时,不仅这个线程会被阻塞,进程中的所有线程都会被阻塞
  • 在纯粹的用户级线程策略中,一个多线程应用程序不能利用多处理技术

(2)内核级线程

在一个纯粹的内核级线程软件中,有关线程管理的所有工作都是由内核完成的,应用程序部分没有进行线程管理的代码,只有一个到内核线程设施的应用程序编程接口(API)。

该方法克服了用户级线程方法的两个基本缺陷

  • 内核可以同时把同一个进程的多个线程调度到多个处理器中;
  • 如果进程中的一个线程被阻塞,内核可以调度同一个进程中的另一个线程。

相比用户级线程它的主要缺点

  • 把控制从一个线程传送到进程中的另一个线程时,需要到内核的状态切换。

某些操作系统采用组合用户级线程和内核级线程的方法,同一个应用程序中的多个线程可以在多个处理器上并行的运行,某个会引起阻塞的系统调用不会阻塞整个进程。如果设计正确,该方法会结合两种线程的优点,同时减少他们的缺点。

猜你喜欢

转载自blog.csdn.net/u013075024/article/details/93298855
今日推荐