天天看点

20170305听课笔记(漫谈进程和线程)

操作系统的关键抽象

20170305听课笔记(漫谈进程和线程)

把 I/O 设备抽象成文件,这样用户就不用直接对硬盘做操作,因为硬盘本身作为一个物理设备,对一般用户来说比较复杂。

物理的主存+文件的概念,就形成了虚拟的存储器。

把 CPU 抽象成指令集, 加上虚拟存储器就形成了进程。

进程加上操作系统就形成了虚拟机。

比喻:厨师做蛋糕

20170305听课笔记(漫谈进程和线程)

做蛋糕的食谱,第一步第二步第三步,告诉你怎么做,就是程序。做蛋糕的原料,就是输入数据。厨师自己就是 CPU ,厨师自己阅读食谱,用原料做蛋糕的一系列动作的总和,就是进程。中途厨师的儿子跑进来了,说是被蜜蜂蛰了,厨师记录下他做到哪一步了,即为保存进程当前状态。拿出急救手册,按其中的指示进行处理,这就是开始另一个进程。

切换进程

20170305听课笔记(漫谈进程和线程)

进程一正在执行——进程一的时间片到期——操作系统保存进程一的上下文——操作系统设置进程二的上下文——开始(继续)执行进程二。

20170305听课笔记(漫谈进程和线程)

如果按中间的执行顺序执行(顺序执行), counter 的最终数值是3,如果按右边的执行顺序执行(乱序执行), counter 的最终数值是2。

20170305听课笔记(漫谈进程和线程)

问题的核心在于 CPU 层面的调度是无法人为控制的,而且在机器层面,很多代码都不是原子操作(不可被中断的)。

临界区:当你访问共享资源的时候,我们把这个区域认为是临界区。

20170305听课笔记(漫谈进程和线程)

解决临界区问题(1):暴力手段,关闭中断

即在操作临界区时不让别的进程访问临界区,但是不能把中断操作开放给应用程序,只能由操作系统自行决定。

20170305听课笔记(漫谈进程和线程)

解决临界区问题(2):用硬件指令来实现锁。

想要访问临界区,需要访问 TestAndSet() 方法,只有 lock 为 false 的时候,才可以访问临界区。

20170305听课笔记(漫谈进程和线程)

解决临界区问题(4):信号量

进入临界区之前,调用 wait() 操作,进入临界区,这时如果再有别的进程需要进入临界区,仍需要调用 wait() 操作,此时 S <= 0 ,只能等待,上一个进程离开临界区,调用 signal() 操作, S++ , S = 0 ,另一个进程就可以作业了。

20170305听课笔记(漫谈进程和线程)

wait() 时先把 s- - ,如果此时 s 指向的 value < 0 ,把当前进程加入到等待列表当中去,当前进程 block() (阻塞),进入等待状态,如果另外一个进程调用了 signal() ,会把 value++ ,判断 value <= 0 ,如果是说明进程在等待,就从里面取出一个进程,把它唤醒。

20170305听课笔记(漫谈进程和线程)

解决临界区问题(6):用信号量解决打印问题

生产者:mutex 表示互斥, empty 用来表示空, full 用来表示满,wait(5) ,empty- - 变为4,wait(1) ,mutex- - 变为0,此时别的线程就不能操作临界区了。消费者同理。

问题:两个线程之间怎么共享变量?

20170305听课笔记(漫谈进程和线程)

多线程的进程:每一个线程都需要维护一套自己的寄存器和堆栈,共享代码、数据和文件。

所以每个线程都需要把自己使用的寄存器的值放入到内存当中。轮到线程运行,就把值从内存放到 CPU 中。

注意:所谓的进程切换本质上是线程的切换,因为 CPU 实际上是被线程占据的。

20170305听课笔记(漫谈进程和线程)

以文字处理器为例:要实现读取用户键盘输入+自动定时的保存文档,如果使用单线程,读取用户输入时就无法保存,保存文档时就无法读取用户输入。如果使用多进程来实现需求,共享数据就变成了一个巨大的问题,这个开销实在太大了。

20170305听课笔记(漫谈进程和线程)

线程的实现

第一种方式:完全在用户空间来实现。

内核空间可以理解成操作系统的内核空间,由操作系统来管理进程的信息、等待队列、就绪队列、运行队列等。

优点:1.保护操作系统不被恶意的应用程序所破坏,防止应用程序直接操作硬件。

2.每次切换线程完全不需要内核的介入,线程的切换会非常快。

缺点:1.自主管理线程非常麻烦。

2.阻塞问题:从操作系统来看,它只看到了一个进程, 假设进程中的某一个线程进行读写操作,进入了阻塞状态,操作系统会认为整个进程进入了阻塞,操作系统会把整个进程放入到阻塞队列中去,那就意味着这个进程不能占据 CPU 了,即使这个进程的其他线程仍然可以运行。

20170305听课笔记(漫谈进程和线程)

第二种方式:在内核中实现线程 。

一个用户线程对应一个内核线程,内核去完成线程的创建和调度(操作系统进程调度)。这样每一个执行序列就需要两个栈,一个是用户态的栈,用户栈就处理不同的函数调用(代码中的函数),如果遇到了系统调用,比如读写文件、中断处理等,需要进入到内核,内核里面对每个线程也要维护一个栈。

优点:进程不会因为某一个线程的阻塞,导致同一个进程的其他线程被阻塞掉。

缺点: 开销过大。

内核空间与用户空间的隔离:可以简单理解为操作系统对外提供的 API 。

20170305听课笔记(漫谈进程和线程)

第三种方式:一种折中的方式,即让一个进程中的多个线程映射到一个内核线程中来,这样内核中创建的线程相对来说就会比较少。这样阻塞的几率也相对比较少。如果线程1阻塞,线程2也会阻塞,但线程3仍然可以运行。这是一种平衡折中。JVM 采用的就是这么一种处理方式。

为什么在 Java 中只提多线程编程,不提多进程编程?因为 JVM 本身就是一个进程,这个进程当中会有很多线程。然后这些线程会映射到操作系统当中的内核线程当中去。操作系统把内核线程调度到各个 CPU 中的核中去执行。

操作系统的书有两本,一本叫《现代操作系统》,另外一个叫《操作系统概念》(戏称恐龙书),但这两本书很难读,因为理论性的东西太多,还是乖乖的看《深入理解计算机》吧。

继续阅读