tingtin

操作系统基础

进程和线程

什么是进程?

进程是操作系统中最重要的抽象概念之一,是资源分配的基本单位,是独立运行的基本单位

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。

上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程一般由以下的部分组成:

  1. 进程控制块PCB,是进程存在的唯一标志,包含进程标识符PID,进程当前状态,程序和数据地址,进程优先级、CPU现场保护区(用于进程切换),占有的资源清单等。

  2. 程序段

  3. 数据段

什么是线程?

  1. 是进程划分的任务,是一个进程内可调度的实体,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

  2. 线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。

  3. 每个线程完成不同的任务,但是属于同一个进程的不同线程之间共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

为什么需要线程?

线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

  1. 进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。

  2. 进程在执行的过程中如果发生阻塞,整个进程就会挂起,即使进程中其它任务不依赖于等待的资源,进程仍会被阻塞。

引入线程就是为了解决以上进程的不足,线程具有以下的优点:

  1. 从资源上来讲,开辟一个线程所需要的资源要远小于一个进程。

  2. 从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间(这种时间的差异主要由于缓存的大量未命中导致)。

  3. 从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同一地址空间,所以一个线程的数据可以被其它线程感知,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步措施)。

简述进程间通信方法

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

不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同。

进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。

简述进程组

进程组即多个进程的集合,进程组有一个组长,组长进程的PID等于进程组的PGID。

多线程模型

  1. 多对一模型。将多个用户级线程映射到一个内核级线程上。该模型下,线程在用户空间进行管理,效率较高。缺点就是一个线程阻塞,整个进程内的所有线程都会阻塞。几乎没有系统继续使用这个模型。

  2. 一对一模型。将内核线程与用户线程一一对应。优点是一个线程阻塞时,不会影响到其它线程的执行。该模型具有更好的并发性。缺点是内核线程数量一般有上限,会限制用户线程的数量。更多的内核线程数目也给线程切换带来额外的负担。linux和Windows操作系统家族都是使用一对一模型。

  3. 多对多模型。将多个用户级线程映射到多个内核级线程上。结合了多对一模型和一对一模型的特点。

进程调度策略的基本设计指标

  1. CPU利用率

  2. 系统吞吐率,即单位时间内CPU完成的作业的数量。

  3. 响应时间。

  4. 周转时间。是指作业从提交到完成的时间间隔。从每个作业的角度看,完成每个作业的时间也是很关键

    • 平均周转时间

    • 带权周转时间

    • 平均带权周转时间

 

进程调度算法

先来先服务调度算法:创建一个任务队列,一旦有新任务就加入这个队列,CPU完成一个任务后就从队列取任务。

短作业(进程)优先调度算法:针对较短的作业,优先调给CPU工作。

时间片轮转算法:每个时间片依次执行一个任务,时间片结束后将该任务放回任务队列。

多级反馈队列:也按时间片轮转算法执行任务,设置n个队列,当第一个队列任务为空,才执行第二个队列,依次类推。 如果在i队列的任务在该时间片执行后没有完成,即插入i+1号队列

进程状态

 

 

进程同步的方法

操作系统中,进程是具有不同的地址空间的,两个进程是不能感知到对方的存在的。有时候,需要多个进程来协同完成一些任务。 当多个进程需要对同一个内核资源进行操作时,这些进程便是竞争的关系,操作系统必须协调各个进程对资源的占用,进程的互斥是解决进程间竞争关系的方法。 进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。 当多个进程协同完成一些任务时,不同进程的执行进度不一致,这便产生了进程的同步问题。需要操作系统干预,在特定的同步点对所有进程进行同步,这种协作进程之间相互等待对方消息或信号的协调关系称为进程同步。进程互斥本质上也是一种进程同步。 进程的同步方法:

  1. 互斥锁

  2. 读写锁

  3. 条件变量

  4. 记录锁(record locking)

  5. 信号量

  6. 屏障(barrier)

线程同步的方法

操作系统中,属于同一进程的线程之间具有相同的地址空间,线程之间共享数据变得简单高效。遇到竞争的线程同时修改同一数据或是协作的线程设置同步点的问题时,

 

 

进程的基本操作

以Unix系统举例:

  1. 进程的创建:fork()。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。fork函数是有趣的(也常常令人迷惑), 因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的 PID。在子进程中,fork 返回 0。因为子进程的 PID 总是为非零,返回值就提供一个明 确的方法来分辨程序是在父进程还是在子进程中执行。

    pid_t fork(void);
    
  2. 回收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。

    pid_t waitpid(pid_t pid, int *statusp, int options);
    
  3. 加载并运行程序:execve 函数在当前进程的上下文中加载并运行一个新程序。

    int execve(const char *filename, const char *argv[], const char *envp[]);
    
  4. 进程终止:

    void exit(int status);

什么是孤儿进程?僵尸进程?

  1. 孤儿进程: 父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被init进程(1号进程)所收养,并由init进程对他们完成状态收集工作。孤儿进程是无害的,不需要进行回收

         2。 僵尸进程:子进程运行结束了而父进程还没有,而且父进程未对子进程进行回收,就会产生僵尸进程

    • 原因:

      • 子进程在完成工作后,会给父进程发送SIGCHILD信号,等待父进程进行处理
      • 如果父进程没有妥善处理,就会产生僵尸进程

    • 解决:两次fork()来避免僵尸进程 

    • 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
    • 通过信号机制来避免僵尸进程

简述线程和进程的区别和联系

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在

  2. 进程在执行过程中拥有独立的地址空间,而多个线程共享进程的地址空间。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)

  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位

  4. 通信:由于同一进程中的多个线程具有相同的地址空间,使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步方法,以保证数据的一致性)。

  5. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。

  6. 进程间不会相互影响;一个进程内某个线程挂掉将导致整个进程挂掉

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

进程和线程的基本API

 

单核cpu同一时刻能处理多少个进程

  • 单核CPU在同一时间只能处理一个进程
  • 多核CPU可以同时执行多个进程,进程个数不高于核数

首先应该明白两个概念:并发和并行

并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。

并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

 死锁

死锁是怎样产生的?

一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。 产生死锁需要满足下面四个条件:

  1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。(一个资源每次只能被一个进程使用)

  2. 占有并等待条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源。

  3. 非抢占条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。

  4. 循环等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链。(若干进程之间形成一种头尾相接的循环等待资源关系)

如何解决死锁问题?

解决死锁的方法即破坏产生死锁的四个必要条件之一,主要方法如下:

  1. 资源一次性分配,这样就不会再有请求了(破坏请求条件)。

  2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源,该进程不投入运行(破坏占有并等待条件)。

  3. 可抢占资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可抢占的条件。

  4. 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件

 

页表

 

简述页表

 

页表用于存储虚拟地址中的虚拟页面号和物理页面号的映射关系。 除此之外,有些页的读写有限制,页表也通过其他存储位,标记该页访问位,是否在内存中(可能被页面置换出去了)等等。

 

简述多级页表

多级页表用于减少内存的占用。以二级页表为例,虚拟地址被分为DIR,PAGE和offset三部分,通过顶级页表和DIR,寻找到该二级页表的起始位置,再通过二级页表的起始位置和PAGE,找到页物理地址,最后加上页偏移,即可得到最终的物理地址。

 

1114. 按序打印

难度简单

给你一个类:

public class Foo {
  public void first() { print("first"); }
  public void second() { print("second"); }
  public void third() { print("third"); }
}

三个不同的线程 A、B、C 将会共用一个 Foo 实例。

  • 线程 A 将会调用 first() 方法
  • 线程 B 将会调用 second() 方法
  • 线程 C 将会调用 third() 方法

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

 

class Foo {

  private AtomicInteger firstJobDone = new AtomicInteger(0);
  private AtomicInteger secondJobDone = new AtomicInteger(0);

  public Foo() {}

  public void first(Runnable printFirst) throws InterruptedException {
    // printFirst.run() outputs "first".
    printFirst.run();
    // mark the first job as done, by increasing its count.
    firstJobDone.incrementAndGet();
  }

  public void second(Runnable printSecond) throws InterruptedException {
    while (firstJobDone.get() != 1) {
      // waiting for the first job to be done.
    }
    // printSecond.run() outputs "second".
    printSecond.run();
    // mark the second as done, by increasing its count.
    secondJobDone.incrementAndGet();
  }

  public void third(Runnable printThird) throws InterruptedException {
    while (secondJobDone.get() != 1) {
      // waiting for the second job to be done.
    }
    // printThird.run() outputs "third".
    printThird.run();
  }
}
class Foo {

    private final CountDownLatch firstDone;
    private final CountDownLatch secondDone;

    public Foo() {
        firstDone = new CountDownLatch(1);
        secondDone = new CountDownLatch(1);
    }

    public void first(Runnable printFirst) throws InterruptedException {
        
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        firstDone.countDown();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        firstDone.await();
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        secondDone.countDown();
    }

    public void third(Runnable printThird) throws InterruptedException {
        secondDone.await();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}

 

class Foo {
   
    private Semaphore s2;
    private Semaphore s3;

    public Foo() {
    
        s2 = new Semaphore(0);
        s3 = new Semaphore(0);
    }

    public void first(Runnable printFirst) throws InterruptedException {
      
        printFirst.run();
        s2.release();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        s2.acquire();
        printSecond.run();
        s3.release();
    }

    public void third(Runnable printThird) throws InterruptedException {
        s3.acquire();
        printThird.run();
    }
}

 

分类:

技术点:

相关文章:

猜你喜欢
  • 2022-01-04
  • 2021-11-08
  • 2021-11-23
相关资源
相似解决方案