搞定操做系统面试,看这篇就够了(一)

1、概述

基本特征

1. 并发

并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。算法

并行须要硬件支持,如多流水线、多核处理器或者分布式计算系统。浏览器

操做系统经过引入进程和线程,使得程序可以并发运行。安全

2. 共享

共享是指系统中的资源能够被多个并发进程共同使用。bash

有两种共享方式:互斥共享和同时共享。服务器

互斥共享的资源称为临界资源,例如打印机等,在同一时间只容许一个进程访问,须要用同步机制来实现对临界资源的访问。多线程

3. 虚拟

虚拟技术把一个物理实体转换为多个逻辑实体。架构

主要有两种虚拟技术:时分复用技术和空分复用技术。并发

多个进程能在同一个处理器上并发执行使用了时分复用技术,让每一个进程轮流占有处理器,每次只执行一小个时间片并快速切换。异步

虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每一个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不须要所有在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。分布式

4. 异步

异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推动。
(文章纯干货 请仔细阅读 整理不易!)

基本功能

1. 进程管理

进程控制、进程同步、进程通讯、死锁处理、处理机调度等。

2. 内存管理

内存分配、地址映射、内存保护与共享、虚拟内存等。

3. 文件管理

文件存储空间的管理、目录管理、文件读写管理和保护等。

4. 设备管理

完成用户的 I/O 请求,方便用户使用各类设备,并提升设备的利用率。

主要包括缓冲管理、设备分配、设备处理、虛拟设备等。

系统调用

若是一个进程在用户态须要使用内核态的功能,就进行系统调用从而陷入内核,由操做系统代为完成。

image

Linux 的系统调用主要有如下这些:

Task Commands
进程控制 fork(); exit(); wait();
进程通讯 pipe(); shmget(); mmap();
文件操做 open(); read(); write();
设备操做 ioctl(); read(); write();
信息维护 getpid(); alarm(); sleep();
安全 chmod(); umask(); chown();

大内核和微内核

1. 大内核

大内核是将操做系统功能做为一个紧密结合的总体放到内核。

因为各模块共享信息,所以有很高的性能。

2. 微内核

因为操做系统不断复杂,所以将一部分操做系统功能移出内核,从而下降内核的复杂性。移出的部分根据分层的原则划分红若干服务,相互独立。

在微内核结构下,操做系统被划分红小的、定义良好的模块,只有微内核这一个模块运行在内核态,其他模块运行在用户态。

由于须要频繁地在用户态和核心态之间进行切换,因此会有必定的性能损失。

中断分类

1. 外中断

由 CPU 执行指令之外的事件引发,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器可以发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

2. 异常

由 CPU 执行指令的内部事件引发,如非法操做码、地址越界、算术溢出等。

3. 陷入

在用户程序中使用系统调用。

2、进程管理

进程与线程

1. 进程

进程是资源分配的基本单位。

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的建立进程和撤销进程,都是指对 PCB 的操做。

下图显示了 4 个程序建立了 4 个进程,这 4 个进程能够并发地执行。

2. 线程

线程是独立调度的基本单位。

一个进程中能够有多个线程,它们共享进程资源。

QQ 和浏览器是两个进程,浏览器进程里面有不少线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新连接从而发起 HTTP 请求时,浏览器还能够响应用户的其它事件。

image

3. 区别

Ⅰ 拥有资源

进程是资源分配的基本单位,可是线程不拥有资源,线程能够访问隶属进程的资源。

Ⅱ 调度

线程是独立调度的基本单位,在同一进程中,线程的切换不会引发进程切换,从一个进程中的线程切换到另外一个进程中的线程时,会引发进程切换。

Ⅲ 系统开销

因为建立或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于建立或撤销线程时的开销。相似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少许寄存器内容,开销很小。

Ⅳ 通讯方面

线程间能够经过直接读写同一进程中的数据进行通讯,可是进程通讯须要借助 IPC。

进程状态的切换

  • 就绪状态(ready):等待被调度

  • 运行状态(running)

  • 阻塞状态(waiting):等待资源

应该注意如下内容:

  • 只有就绪态和运行态能够相互转换,其它的都是单向转换。就绪状态的进程经过调度算法从而得到 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完以后就会转为就绪状态,等待下一次调度。

  • 阻塞状态是缺乏须要的资源从而由运行状态转换而来,可是该资源不包括 CPU 时间,缺乏 CPU 时间会从运行态转换为就绪态。

进程调度算法

不一样环境的调度算法目标不一样,所以须要针对不一样环境来讨论调度算法。

1. 批处理系统

批处理系统没有太多的用户操做,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

1.1 先来先服务 first-come first-serverd(FCFS)

按照请求的顺序进行调度。

有利于长做业,但不利于短做业,由于短做业必须一直等待前面的长做业执行完毕才能执行,而长做业又须要执行很长时间,形成了短做业等待时间过长。

1.2 短做业优先 shortest job first(SJF)

按估计运行时间最短的顺序进行调度。

长做业有可能会饿死,处于一直等待短做业执行完毕的状态。由于若是一直有短做业到来,那么长做业永远得不到调度。

1.3 最短剩余时间优先 shortest remaining time next(SRTN)

按估计剩余时间最短的顺序进行调度。

2. 交互式系统

交互式系统有大量的用户交互操做,在该系统中调度算法的目标是快速地进行响应。

2.1 时间片轮转

将全部就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程能够执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便中止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系:

  • 由于进程切换都要保存进程的信息而且载入新进程的信息,若是时间片过小,会致使进程切换得太频繁,在进程切换上就会花过多时间。

  • 而若是时间片过长,那么实时性就不能获得保证。

2.2 优先级调度

为每一个进程分配一个优先级,按优先级进行调度。

为了防止低优先级的进程永远等不到调度,能够随着时间的推移增长等待进程的优先级。

2.3 多级反馈队列

一个进程须要执行 100 个时间片,若是采用时间片轮转调度算法,那么须要交换 100 次。

多级队列是为这种须要连续执行多个时间片的进程考虑,它设置了多个队列,每一个队列时间片大小都不一样,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,以前的进程只须要交换 7 次。

每一个队列优先权也不一样,最上面的优先权最高。所以只有上一个队列没有进程在排队,才能调度当前队列上的进程。

能够将这种调度算法当作是时间片轮转调度算法和优先级调度算法的结合。

3. 实时系统

实时系统要求一个请求在一个肯定时间内获得响应。

分为硬实时和软实时,前者必须知足绝对的截止时间,后者能够容忍必定的超时。

进程同步

1. 临界区

对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源,每一个进程在进入临界区以前,须要先进行检查。

// entry section
// critical section;
// exit section
复制代码

2. 同步与互斥

  • 同步:多个进程按必定顺序执行;

  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

3. 信号量

信号量(Semaphore)是一个整型变量,能够对其执行 down 和 up 操做,也就是常见的 P 和 V 操做。

  • down : 若是信号量大于 0 ,执行 -1 操做;若是信号量等于 0,进程睡眠,等待信号量大于 0;

  • up :对信号量执行 +1 操做,唤醒睡眠的进程让其完成 down 操做。

down 和 up 操做须要被设计成原语,不可分割,一般的作法是在执行这些操做的时候屏蔽中断。

若是信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

void P2() {
    down(&mutex);
    // 临界区
    up(&mutex);
}
复制代码

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才能够放入物品;只有缓冲区不为空,消费者才能够拿走物品。

由于缓冲区属于临界资源,所以须要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,须要记录缓冲区中物品的数量。数量可使用信号量来进行统计,这里须要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才能够放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才能够取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。若是这么作了,那么可能会出现这种状况:生产者对缓冲区加锁后,执行 down(empty) 操做,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,由于生产者对缓冲区加锁了,消费者就没法执行 up(empty) 操做,empty 永远都为 0,致使生产者永远等待下,不会释放锁,消费者所以也会永远等待下去。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer() {
    while(TRUE) {
        down(&full);
        down(&mutex);
        int item = remove_item();
        consume_item(item);
        up(&mutex);
        up(&empty);
    }
}
复制代码

4. 管程

使用信号量机制实现的生产者消费者问题须要客户端代码作不少控制,而管程把控制的代码独立出来,不只不容易出错,也使得客户端代码调用更容易。

c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码经过调用这两个方法来解决生产者-消费者问题。

monitor ProducerConsumer
    integer i;
    condition c;

    procedure insert();
    begin
        // ...
    end;

    procedure remove();
    begin
        // ...
    end;
end monitor;
复制代码

管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在没法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。

管程引入了 条件变量 以及相关的操做:wait()signal() 来实现同步操做。对条件变量执行 wait() 操做会致使调用进程阻塞,把管程让出来给另外一个进程持有。signal() 操做用于唤醒被阻塞的进程。

使用管程实现生产者-消费者问题

// 管程// 管程
monitor ProducerConsumer
    condition full, empty;
    integer count := 0;
    condition c;

    procedure insert(item: integer);
    begin
        if count = N then wait(full);
        insert_item(item);
        count := count + 1;
        if count = 1 then signal(empty);
    end;

    function remove: integer;
    begin
        if count = 0 then wait(empty);
        remove = remove_item;
        count := count - 1;
        if count = N -1 then signal(full);
    end;
end monitor;

// 生产者客户端
procedure producer
begin
    while true do
    begin
        item = produce_item;
        ProducerConsumer.insert(item);
    end
end;

// 消费者客户端
procedure consumer
begin
    while true do
    begin
        item = ProducerConsumer.remove;
        consume_item(item);
    end
end;
复制代码

经典同步问题

生产者和消费者问题前面已经讨论过了。

1. 读者-写者问题

容许多个进程同时对数据进行读操做,可是不容许读和写以及写和写操做同时发生。

一个整型变量 count 记录在对数据进行读操做的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
    while(TRUE) {
        down(&count_mutex);
        count++;
        if(count == 1) down(&data_mutex); // 第一个读者须要对数据进行加锁,防止写进程访问
        up(&count_mutex);
        read();
        down(&count_mutex);
        count--;
        if(count == 0) up(&data_mutex);
        up(&count_mutex);
    }
}

void writer() {
    while(TRUE) {
        down(&data_mutex);
        write();
        up(&data_mutex);
    }
}
复制代码

如下内容由 @Bandi Yugandhar 提供。

The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer).

int readcount, writecount;                   //(initial value = 0)
semaphore rmutex, wmutex, readLock, resource; //(initial value = 1)

//READER
void reader() {
<ENTRY Section>
 down(&readLock);                 // reader is trying to enter
 down(&rmutex);                  // lock to increase readcount
  readcount++;                 
  if (readcount == 1)          
   down(&resource);              //if you are the first reader then lock the resource
 up(&rmutex);                  //release for other readers
 up(&readLock);                 //Done with trying to access the resource

<CRITICAL Section>
//reading is performed

<EXIT Section>
 down(&rmutex);                  //reserve exit section - avoids race condition with readers
 readcount--;                       //indicate you're leaving
  if (readcount == 0)          //checks if you are last reader leaving
   up(&resource);              //if last, you must release the locked resource
 up(&rmutex);                  //release exit section for other readers
}

//WRITER
void writer() {
  <ENTRY Section>
  down(&wmutex);                  //reserve entry section for writers - avoids race conditions
  writecount++;                //report yourself as a writer entering
  if (writecount == 1)         //checks if you're first writer
   down(&readLock);               //if you're first, then you must lock the readers out. Prevent them from trying to enter CS
  up(&wmutex);                  //release entry section

<CRITICAL Section>
 down(&resource);                //reserve the resource for yourself - prevents other writers from simultaneously editing the shared resource
  //writing is performed
 up(&resource);                //release file

<EXIT Section>
  down(&wmutex);                  //reserve exit section
  writecount--;                //indicate you're leaving
  if (writecount == 0)         //checks if you're the last writer
   up(&readLock);               //if you're last writer, you must unlock the readers. Allows them to try enter CS for reading
  up(&wmutex);                  //release exit section
}
复制代码

We can observe that every reader is forced to acquire ReadLock. On the otherhand, writers doesn’t need to lock individually. Once the first writer locks the ReadLock, it will be released only when there is no writer left in the queue.

From the both cases we observed that either reader or writer has to starve. Below solutionadds the constraint that no thread shall be allowed to starve; that is, the operation of obtaining a lock on the shared data will always terminate in a bounded amount of time.

int readCount;                  // init to 0; number of readers currently accessing resource

// all semaphores initialised to 1
Semaphore resourceAccess;       // controls access (read/write) to the resource
Semaphore readCountAccess;      // for syncing changes to shared variable readCount
Semaphore serviceQueue;         // FAIRNESS: preserves ordering of requests (signaling must be FIFO)

void writer() { 
    down(&serviceQueue);           // wait in line to be servicexs
    // <ENTER>
    down(&resourceAccess);         // request exclusive access to resource
    // </ENTER>
    up(&serviceQueue);           // let next in line be serviced

    // <WRITE>
    writeResource();            // writing is performed
    // </WRITE>

    // <EXIT>
    up(&resourceAccess);         // release resource access for next reader/writer
    // </EXIT>
}

void reader() { 
    down(&serviceQueue);           // wait in line to be serviced
    down(&readCountAccess);        // request exclusive access to readCount
    // <ENTER>
    if (readCount == 0)         // if there are no readers already reading:
        down(&resourceAccess);     // request resource access for readers (writers blocked)
    readCount++;                // update count of active readers
    // </ENTER>
    up(&serviceQueue);           // let next in line be serviced
    up(&readCountAccess);        // release access to readCount

    // <READ>
    readResource();             // reading is performed
    // </READ>

    down(&readCountAccess);        // request exclusive access to readCount
    // <EXIT>
    readCount--;                // update count of active readers
    if (readCount == 0)         // if there are no readers left:
        up(&resourceAccess);     // release resource access for all
    // </EXIT>
    up(&readCountAccess);        // release access to readCount
}
复制代码

2. 哲学家进餐问题

五个哲学家围着一张圆桌,每一个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,须要先拿起本身左右两边的两根筷子,而且一次只能拿起一根筷子。

下面是一种错误的解法,考虑到若是全部哲学家同时拿起左手边的筷子,那么就没法拿起右手边的筷子,形成死锁。

#define N 5

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}
复制代码

为了防止死锁的发生,能够设置两个条件:

  • 必须同时拿起左右两根筷子;

  • 只有在两个邻居都没有进餐的状况下才容许进餐。

#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N // 右邻居
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N];                // 跟踪每一个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥
semaphore s[N];              // 每一个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think();
        take_two(i);
        eat();
        put_two(i);
    }
}

void take_two(int i) {
    down(&mutex);
    state[i] = HUNGRY;
    test(i);
    up(&mutex);
    down(&s[i]);
}

void put_two(i) {
    down(&mutex);
    state[i] = THINKING;
    test(LEFT);
    test(RIGHT);
    up(&mutex);
}

void test(i) {         // 尝试拿起两把筷子
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}
复制代码

进程通讯

进程同步与进程通讯很容易混淆,它们的区别在于:

  • 进程同步:控制多个进程按必定顺序执行;

  • 进程通讯:进程间传输信息。

进程通讯是一种手段,而进程同步是一种目的。也能够说,为了可以达到进程同步的目的,须要让进程进行通讯,传输一些进程同步所须要的信息。

1. 管道

管道是经过调用 pipe 函数建立的,fd[0] 用于读,fd[1] 用于写。

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

它具备如下限制:

  • 只支持半双工通讯(单向交替传输);

  • 只能在父子进程中使用。

2. FIFO

也称为命名管道,去除了管道只能在父子进程中使用的限制。

#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
复制代码

FIFO 经常使用于客户-服务器应用程序中,FIFO 用做汇聚点,在客户进程和服务器进程之间传递数据。

3. 消息队列

相比于 FIFO,消息队列具备如下优势:

  • 消息队列能够独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;

  • 避免了 FIFO 的同步阻塞问题,不须要进程本身提供同步方法;

  • 读进程能够根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。

4. 信号量

它是一个计数器,用于为多个进程提供对共享数据对象的访问。

5. 共享存储

容许多个进程共享一个给定的存储区。由于数据不须要在进程之间复制,因此这是最快的一种 IPC。

须要使用信号量用来同步对共享存储的访问。

多个进程能够将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用使用内存的匿名段。

6. 套接字

与其它通讯机制不一样的是,它可用于不一样机器间的进程通讯。

(文章 实在太长 分两篇完成 )续......

推荐阅读:阿里腾讯Android开发十年,到中年危机就只剩下这套移动架构体系了!
相关文章
相关标签/搜索