线程与进程的区别

进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。

  1. 进程之间信息难以共享。因为出去共享代码段,父子进程之间并未共享内存(写时复制),因此必须采用一些进程间通信技术,在进程间进行信息交换。
  2. 调用fork()创建进程的代价相对较高,即使使用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性。
  3. 线程之间能够方便、快速的共享信息。只需要将数据复制到共享变量(全局或堆)中。
  4. 创建线程比创建进程通常很多。线程间是共享虚拟地址空间的,无需采用写时复制技术,也无需复制页表。
  5. 线程共享虚拟地址空间,在代码段执行各自的代码,在栈空间每个线程都有自己的区域。

线程之间共享的资源和非共享的资源

共享资源

  1. 进程id和父进程id
  2. 进程组id和会话id
  3. 用户id和用户组id
  4. 文件描述符表
  5. 信号处理行为,当一个线程对某个信号设置了处理方式,那么所有线程共享这个处理
  6. 文件系统的相关信息:文件权限掩码、当前工作目录等
  7. 虚拟地址空间(栈和代码段除外)

非共享资源

  1. 线程ID
  2. 信号掩码
  3. 线程特有数据
  4. error变量
  5. 实时调度策略和优先级
  6. 栈、本地变量和函数的调用链接信息。

线程的调度

在一个系统上一般会同时运行交互式进程和后台进程,标准的内核调度算法一般能够为这些进程提供足够的性能和响应度。但实时应用对调度器具有更加严格的要求。

  1. 实时应用必须要为外部输入提供最大响应时间。在很多情况下,这些最大响应时间必须非常短。比如交通导航系统的慢速响应就可能导致一个灾难。内核必须要提供工具让高优先级进程能快速取得CPU的控制权,抢占当前运行的所有进程。
  2. 一些时间关键的应用程序可能需要采取其他措施避免不可接收的延迟。比如为了避免页面错误而引起的延迟,应用程序会调用mlockmlockall将所有虚拟内存锁定在RAM中。
  3. 高优先级的进程应该能够保持互斥的访问CPU直至它完成或自动释放CPU。
  4. 实时应用应该能够精确地控制其组件进程的调度顺序。

SUSv3规定的实时进程调度API部分满足了这些要求。这个API提供了两个实时调度策略SCHED_RRSCHED_FIFO。使用这两种策略中的任意一种进行调度的进程的优先级要高于使用标准循环时间分享策略来调度的进程,实时调度API使用常量SCHED_OTHER来标识这种循环时间分享策略。

每个实时策略允许一个优先级范围。在每个调度策略中,拥有高优先级的可运行进程在尝试访问CPU时总是优先于优先级较低的进程。

对于多处理器Linux系统来讲,高优先级的可运行进程总是优先于优先级较低的进程的规则并不适用。在多处理器系统中,各个CPU拥有独立的运行队列,并且每个CPU的运行队列中的进程的优先级都局限于该队列。比如一个双处理器系统中运行者三个进程,进程A的实时优先级是20并且它位于CPU0的等待队列中,而该CPU当前在正在运行优先级为30的进程B,即使CPU 1正在运行优先级为10的进程C,进程A还是需要等待CPU 0。

包含多个进程的实时应用可以使用CPU亲和力API来避免这种调度行为可能引起的问题。如在一个四处理器系统中,所有非关键的进程可以分配到一个CPU中,让其他三个CPU处理实时应用。

Linux提供了99个实时优先级,数值从1最低到99最高,并且这个取值范围同时适用于两个实时调度策略。

每个策略中的优先级是等价的。这意味着如果两个进程拥有同样的优先级,一个进程采用了SCHED_RR的调度策略,另一个进程采用了SCHED_FIFO的调度策略,那么两个都符合运行的条件,至于到底运行哪个则取决与她们被调度的顺序了。

实际上,每个优先级级别都维护着一个可运行的进程队列,下一个运行的进程是从优先级最高的非空队列的对头中选取出来的

SCHED_RR策略

SCHED_RR策略中,优先级相同的进程以循环时间分享的方式执行。进程每次使用CPU的时间为一个固定长度的时间片。

一旦被调度执行之后,使用SCHED_RR策略的进程会保持对CPU的控制直到下列条件中的一个得到满足:

  1. 到达时间片的终点。
  2. 自愿放弃CPU,比如执行一个阻塞式系统调用或调用了sched_yield()系统调用。
  3. 进程终止。
  4. 被一个优先级更高的进程抢占。

对于上面列出的前两个事件,当运行在SCHED_RR策略下的进程丢掉CPU之后将会被放置在与其优先级级别对应的队列的队尾

在最后一种情况中,当优先级更高的进程执行结束之后,被抢占的进程会继续执行直到其时间片的剩余部分被消耗完(即被抢占的进程仍然位于其优先级级别对应的队列的对头),这个前提是优先级更高的进程执行结束之后,没有下一个优先级更高的进程,否则会继续被抢占。

SCHED_RRSCHED_FIFO两种策略中,当前运行的进程可能会因为下面某种原因而被抢占:

  1. 之前被阻塞的高优先级进程解除阻塞了。
  2. 另一个进程的优先级被提高到一个级别高于当前运行进程的优先级的优先级,然后被另一个进程抢占。
  3. 当前运行的进程的优先级被降低到低于其他可运行进程的优先级。

SCHED_RRSCHED_OTHER对比:

  1. 相同点:都是通过循环时间分享调度。
  2. 差异点:SCHED_RR策略具有严格的优先级级别,高优先级的进程总是优先于优先级较低的进程。SCHED_RR策略允许精确控制进程被调用的顺序。

SCHED_RR策略与标准的循环时间分享调度算法(SCHED_OTHER)类似,即它也允许优先级相同的一组进程分享CPU时间。

他们之间最重要的差别在于SCHED_RR策略存在严格的优先级级别,高优先级的进程总是优先于优先级较低的进程

SCHED_OTHER策略中,低nice值的进程不会独占CPU,它仅仅在调度决策时为进程提供一个较大权重。一个优先级较低(高nice值)的进程总是至少会用到一些CPU时间的。

SCHED_FIFO策略

SCHED_FIFO先进先出策略与SCHED_RR策略类似,最主要的差别在不存在时间片,一旦一个SCHED_FIFO进程获得了CPU的控制权之后,它就会一直执行直到下面某个条件满足:

  1. 自动放弃CPU。
  2. 进程终止。
  3. 被一个优先级更高的进程抢占。

在第1种情况中,进程会被放置在与其优先级级别对应的队列的队尾。

在最后一种情况中,当高优先级进程执行结束之后,被抢占的进程会继续执行(即被抢占的进程位于与其优先级级别对应的队列的对头)。

IPC

Inter-Process Communication,进程间通信。

匿名管道

匿名管道是半双工的。

管道只能在具有公共祖先的两个进程之间使用。通常一个管道由一个进程创建,在进程fork之后,这个管道就能在父进程和子进程之间使用了。

单个进程中的管道几乎没有任何用处。通常进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道。

fork之后做什么却决于我们想要的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端,子进程关闭写端。

当管道的一端被关闭之后,下列两条规则起作用

  1. 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
  2. 如果写一个读端已被关闭的管道,则会产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno设置为EPIPE

在写管道时,常量PIPE_BUF规定了内核的管道缓冲区大小。如果对管道调用write,而且要求写入的字节数小于等于PIPE_BUF,则此操作不会与其他进程对同一管道的write操作交叉进行。但是若有多个进程同时写一个管道,而且我们要求写的字节数超过PIPE_BUF,那么我们所写的数据可能会与其他进程缩写的数据相互交叉。

FIFO

消息队列

共享内存

信号

信号量