操作系统进程

进程

进程是资源分配的基本单位,他是程序运行时的一个实例。
程序运行时,系统会创建一个进程,并分配相关的资源。

程序是静态的,进程是动态的。一个程序可以对应多个进程,一个进程可以包括多个程序。

进程=程序+数据+状态信息。

PCB(进程控制块)

每个进程都有一个进程控制块,他是进程的唯一标识
进程块描述进程的基本信息和运行状态。所谓的进程的创建与销毁,就是对PCB的创建与销毁。

各个进程块在内存中应该是以链表的形式存储的,因为进程块需要频繁的进行插入与删除。

进程的创建

  • 给新进程分配一个唯一标识以及进程控制块(没有被使用的)
  • 为进程分配独立地址空间
  • 初始化进程控制块:设置默认值(如状态为New…)
  • 设置相应地队列指针。如:把新进程加到就绪队列链表中
  • 主要操作是UNIX中:fork/execWindows中:CreateProcess

进程的生命周期

title
最主要的就是就绪,运行,阻塞三个状态。
操作系统创建一个进程完成后,并为其分配了除了cpu之外的所有的资源,那么进程进入就绪(ready)状态,当CPU处于空闲的时候,就绪状态的进程占用CPU,进入运行(running)状态。正在执行的进程,由于等待某个事件发生而无法执行时,便放弃CPU而处于阻塞(waiting)状态

  • ready->running
    当没有其他进程占用CPU,所有的ready进程就可以去竞争CPU,获得CPU的进程进入running状态。

  • running->waiting
    正在执行的进程,因为等待某个事件发生而放弃CPU时,进入waiting状态。比如I/O阻塞,申请缓冲区不能满足、等待信件等。

  • running->ready
    处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完或更高优先级的进程抢占而不得不让出处理机,于是进程从执行状态转变成就绪状态。

  • waiting->ready
    当等待事件完成的时候,从阻塞进入就绪态。
    需要正在运行的进程对他唤醒。

进程切换

因为所有的进程都有自己独立的地址空间。

  • 页表以及地址空间的切换。

  • 寄存器,程序计数器,堆栈的切换。

进程上下文切换过程:

  • 保存现场。暂停当前进程,从运行态变为其他状态,保存当前进程的上下文,包括CPU寄存器状态,程序计数器状态等。保存在PCB中。

  • 选取进程。调度另一个进程从就绪转为运行。

  • 恢复现场。从内存中恢复下一个要执行的进程的上下文,恢复该进程原来的状态到寄存器,恢复程序执行上一次暂停的地方。从PCB中取。

    在进程切换的过程中,页表会改变,地址空间会改变,高速缓存中的存储的数据过期,也需要进行切换。

进程挂起

进程从内存转移到磁盘上
进程挂起状态包括阻塞挂起(在外存,处于阻塞)和就绪挂起(在外存,就绪状态)。

进程挂起状态转换:

  • 阻塞到阻塞挂起。内存不够时,将阻塞状态的进程移到外存,变为阻塞挂起状态。
  • 就绪到就绪挂起。有高优先级阻塞和低优先级就绪,那么将低优先级挂起。
  • 运行到就绪挂起。

挂起是将进程从内存转移到磁盘,而阻塞是由于资源得不到满足暂时无法获取CPU,还是在内存的。

状态队列

操作系统中维护了多个队列,不同的队列来表示不同的状态。
就绪队列,阻塞队列,运行队列等。
方便操作系统管理进程。
title

线程

当我们并发的需求时,如果采用多进程的话,因为进程每个进程都有自己的独立空间,进程间通信麻烦,还有进程切换的时候需要进行保护现场恢复现场,十分耗费资源,效率低下。
于是,引入了线程。

线程是轻量级的进程。所有的线程共享进程的地址空间,进程间开销小,通信方便。
进程中的所有线程共享代码,文件等资源。
但是,他们都有自己的堆栈,寄存器等

title

进程是资源分配的角色,线程是执行功能的角色。
一个线程崩溃,整个进程崩溃。因此,在对于安全性过高的场合,我们一般使用进程来解决并发问题。比如说,我们的浏览器,每开一个界面,就创建一个进程。

OS中两种线程

根据操作系统能够感知到线程,分为用户线程以及内核线程。

内核线程

内核完成线程的创建以及管理。
内核分配CPU是以线程为单位的。

优点:

  • 一个线程阻塞不会导致整个进程阻塞。
  • 内核会为每一个线程分配CPU,对于多线程的进程,时间片时间大大增加。

缺点:

  • 线程切换要从用户态转移到内核态,耗费大,速度慢。

用户线程

用户级的线程库完成线程的创建以及管理。
内核资源的分配仍然是按照进程(用户进程)进行分配的。
不依赖于操作系统的内核,操作系统感受不到用户线程的存在

缺点:

  • 因此,对于操作系统来说,这个用户线程所属的进程是没有线程的,因此,一个线程的阻塞将导致整个进程的阻塞,因为,对于操作系统来说,他看到的只是这个进程,这个用户线程阻塞,对操作系统来说就是整个进程阻塞,所以这个进程将会阻塞。
  • 因为没有操作系统的管制,一个用户线程拿到了分配给这个进程的时间片,他会一直霸占着,除非她主动放弃,或者到这个时间片结束,可能会导致别的用户线程没有机会执行。

优点:

  • 但是,用户线程切换不需要从用户态转到内核态,消耗小,速度快。

总的来说,对于用户线程,操作系统是感受不到,还是会把它看作一个进程来进行处理

内核线程与用户线程

多线程模型

将用户线程与内核线程绑定。主要有一对一,多对一,以及多对多。操作系统中主要使用多对多。

多对一

多个用户线程与一个内核线程绑定。

缺点是一个线程阻塞,这多个用户线程都会被阻塞。

一对一

一个用户线程绑定一个内核线程。

缺点是每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。

多对多

将 n 个用户级线程映射到 m 个内核级线程上,要求 m <= n。

不会出现一个用户线程阻塞,所有线程都阻塞的情况。

详解多线程模型

与进程区别

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立空间,每创建一个进程,都要为他分配独立的地址空间,花费很大。而线程是共享进程地址空间的,花费要小很多。
  • 进程之间的通信需要以通信的方式(IPC)进行,需要通过内核来通信。而线程可以通过共享变量等方式进行。
  • 进程之间切换时间比线程之间切换时间要大得多。因为进程之间页表是不同的,需要切换页表,开销比较大。因为各个进程页表不同,TLB,缓存信息可能都需要重新加载。而线程是共享的。
  • 一个进程死掉,对其他进程没有影响;一个线程死掉,整个进程就会崩溃。
    当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃。

线程切换

与进程上下文切换不同的是,线程上下文切换没有页表以及地址空间的切换,因为同一个进程的线程共享同一个地址空间。只需要进行程序计数器,寄存器,以及线程的堆栈的切换。

fork and exec

linux的fork 和exec 函数。

fork() 复制出一个子进程,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通信来实现,这将是我们下面的内容。既然它们如此相象,系统如何来区分它们呢?这由函数的返回值来决定的。对于父进程, fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。在操作系统中,我们用ps函数就可以看到不同的进程号,对父进程而言,它的进程号是由比它更低 层的系统调用赋予的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。在程序设计中,父进程和子进程都要调用函数fork()下面的代码,而我们就是利用fork()函数对父子进程的不同返回值用if…else…语句来实现让父子进程完成不同的功能。

exec将替换现有进程,执行exec的程序。

一个进程一旦调用exec类函数,它本身就”死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。

僵尸进程和孤儿进程

正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。

一个子进exit()之后,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。这样,一个进程才算是完全终止掉。

当一个父进程迟迟没有调用wait(),这个子进程内存中保存的信息就迟迟不会释放,包括进程号也不会释放,操作系统的进程号是有限的,因此僵尸进程的危害很大。

当一个父进程退出,子进程还在运行,那么子进程将会称为孤儿进程。孤儿进程会被init进程处理,使用wait()完成进程的终止等,因此,孤儿进程是没有坏处的。

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。