进程概述

又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深刻学习操做系统呢? 今天就这个问题开始,来谈谈操做系统中能够说是最重要的一个概念--进程html

操做系统最主要的两个职能是管理各类资源和为应用程序提供系统调用接口。这其中关键的部分是,cpu到进程的抽象,物理内存到地址空间(虚拟内存)的抽象,磁盘到文件的抽象,而其中后两部分以进程为基础,因此嘛,咱重点来讨论进程,以及与进程密切相关的线程。linux

.先说说概念程序员

进程(process)算法

狭义的定义:进程就是一段程序的执行过程。shell

广义定义:进程是一个具备必定独立功能的程序关于某次数据集合的一次运行活动,它是操做系统分配资源的基本单元。编程

简单来说进程的概念主要有两点:第一,进程是一个实体。每个进程都有它本身的地址空间,通常状况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程当中调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,咱们称其为进程。数组

进程状态:进程有三个状态,就绪,运行和阻塞。就绪状态其实就是获取了除cpu外的全部资源,只要处理器分配资源立刻就能够运行。运行态就是获取了处理器分配的资源,程序开始执行,阻塞态,当程序条件不够时,须要等待条件知足时候才能执行,如等待I/O操做的时候,此刻的状态就叫阻塞态。缓存

说说程序,程序是指令和数据的有序集合,其自己没有任何运动的含义,是一个静态的概念,而进程则是在处理机上的一次执行过程,它是一个动态的概念。进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。安全

线程(thread)session

一般在一个进程中能够包含若干个线程,固然一个进程中至少有一个线程,否则没有存在的意义。线程能够利用进程所拥有的资源,在引入线程的操做系统中,一般都是把进程做为分配资源的基本单位,而把线程做为独立运行和独立调度的基本单位,因为线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提升系统多个程序间并发执行的程度。

多线程(multiThread)

在一个程序中,这些独立运行的程序片断叫做“线程”(Thread),利用它编程的概念就叫做“多线程处理”。多线程是为了同步完成多项任务,不是为了提升运行效率,而是为了提升资源使用效率来提升系统的效率。线程是在同一时间须要完成多项任务的时候实现的。

最简单的比喻多线程就像火车的每一节车箱,而进程则是火车。车箱离开火车是没法跑动的,同理火车也不可能只有一节车箱。多线程的出现就是为了提升效率。

2、说说区别

1、进程与线程的区别:

进程和线程的主要差异在于它们是不一样的操做系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享某些变量的并发操做,只能用线程,不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

3) 另外,进程在执行过程当中拥有独立的内存单元,而多个线程共享内存,从而极大地提升了程序的运行效率。

4) 线程在执行过程当中与进程仍是有区别的。每一个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。可是线程不可以独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分能够同时执行。但操做系统并无将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

3、说说优缺点

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP(多核处理机)机器上运行,而进程则能够跨机器迁移。

4、说说进程和线程的细节,底层构成 和 调度

(一)进程相关的数据结构

为了管理进程,内核必须对每一个进程所作的事情进行清楚的描述,例如,内核必须知道进程的优先级,它是在CPU上运行仍是由于某些事而被阻塞,给它分配了什么样的地址空间,容许它访问哪一个文件等。

这些正是进程描述符的做用---进程描述符都是task_struct 数据结构,它的字段包含了与一个进程相关的全部信息。下图显示了Linux进程描述符

谈谈进程的基本信息。

1)标识一个进程--PID

每一个进程都必须拥有它本身的进程描述符;进程和进程描述符之间有很是严格的一一对应关系,因此咱们能够方便地使用32位进程描述符地址标识进程。

进程描述符指针(task_struct*)指向这些地址。内核对进程的大部份引用都是经过进程描述符指针进行的。

另外一方面,类Unix橾做系统容许用户使用一个叫作进程标识符processID(PID)的数来标识进程,PID存放在task_struct的pid字段中。PID被顺序编号,新建立进程的PID一般是前一个进程的PID加1。不过,PID的值有一个上限,当内核使用的PID达到这个峰值的时候,就必须开始循环使用已闲置的小PID号。在缺省状况下,最大的PID号是32767。

系统管理员能够经过往/proc/sys/kernel/pid_max 这个文件中写入一个更小的值来减少PID的上限值,使PID的上限小于32767。在64位体系结构中,系统管理员能够把PID的上限扩大到4194304。

Linux只支持轻量级进程,不支持线程,但为了弥补这样的缺陷,Linux引入线程组的概念。一个线程组中的全部线程使用和该线程组的领头线程相同的PID,也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid值,所以,一个多线程应用的全部线程共享相同的PID。绝大多数进程都属于一个线程组;而线程组的领头线程其tgid与pid的值相同,于是getpid()系统调用对这类进程所起的做用和通常进程是同样的。

因此,咱们得出一个重要的结论,Linux虽不支持线程,可是它有具有支持线程的操做系统的全部特性,后面讲解轻量级进程的概念中还会详细讨论。

2)进程描述符定位

进程是动态实体,其生命周期范围从几毫秒到几个月,所以内核必须同时处理不少进程,并把对应的进程描述符放在动态内存中,而不是放在永久分配给内核的内存区(3G之上的线性地址)。

那么,怎么找到被动态分配的进程描述符呢?咱们须要在3G之上线性地址的内存区为每一个进程设计一个块—thread_union。

对每一个进程来讲,咱们须要给其分配两个页面,即8192个字节的块,Linux把两个不一样数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另外一个是紧挨着进程描述符的小数据结构thread_info,叫作线程描述符。

考虑到效率问题,内核让这8k的空间占据连续两个页框并让第一个页框的起始地址是2^13的倍数。当几乎没有可用的动态内存空间时,就会很难找到这样的两个连续页框,由于空闲空间可能存在大量的碎片(注意,这里是物理空间,见“伙伴系统算法”博文)。所以,在80x86体系结构中,在编译时能够进行设置,以使内核栈和线程描述符跨越一个单独的页框(由于主要存在的单页的碎片)。在“Linux中的分段”的博文中咱们已经知道,内核态的进程访问处于内核数据段的栈,也就是咱们Linux在3G以上内存空间为每一个进程设计这么一个栈的目的,这个栈不一样于用户态的进程所用的栈。由于内核控制路径使用不多的栈,所以只须要几千个字节的内核态堆栈。因此,对栈和thread_info来讲,8KB足够了。不过,若是只使用一个页框存放这两个结构的话,内核要采用一些额外的栈以防止中断和异常的深度嵌套而引发的溢出。

下图显示了在2页(8KB)内存区中存放两种数据结构的方式。线程描述符驻留于这个内存区的开始位置,而栈从末端向下增加。该图还显示了如何经过task字段与task_struct结构相互关联。

struct thread_info {

struct task_struct    *task;        /* main task structure */

struct exec_domain    *exec_domain;    /* execution domain */

unsigned long        flags;        /* low level flags */

unsigned long        status;        /* thread-synchronous flags */

__u32            cpu;        /* current CPU */

__s32            preempt_count; /* 0 => preemptable, <0 => BUG */

mm_segment_t        addr_limit;    /* thread address space:0-0xBFFFFFFF for user-thead  0-0xFFFFFFFF for kernel-thread*/

struct restart_block    restart_block;

unsigned long           previous_esp;   /* ESP of the previous stack in caseof nested (IRQ) stacks*/

__u8            supervisor_stack[0];

};

esp为CPU栈指针寄存器,用来存放栈顶单元的地址。在80x86系统中,栈起始于末端,并朝这个内存区的起始方向增加。从用户态切换到内核态之后,进程的内核栈老是空的,所以,esp寄存器指向这个栈的顶端。

一旦数据写入堆栈,esp的值就递减。特别要注意,这里的数据是指内核数据,其实用得不多,因此大多数时候这个内核栈是空的。由于thread_info

结构是52个字节的长度,因此内核栈能扩展到8140个字节。C语言使用下列联合结构,方便地表示一个进程的线程描述符和内核栈:

union thread_union {

struct thread_info thread_info;

unsigned long stack[2048]; /* 1024 for 4KB stacks */

};

内核使用alloc_thread_info 和 free_thread_info宏分配和释放存储thread_info结构和内核栈的内存区。

3)标识当前进程

咱们再从效率的观点来看,刚才所讲的thread_info结构与内核态堆栈之间的紧密结合提供的主要好处还在:内核很容易从esp寄存器的值得到当前在CPU上正在运行进程的thread_info结构的地址。事实上,若是thread_union的长度是8K(213字节),则内核屏蔽掉esp的低13位有效位就能够得到thread_info结构的基地址;而若是thread_union的长度是4K,内核须要蔽掉esp的低12位有效位。这项工做由current_thread_info()函数来完成,它产生以下一些汇编指令:

movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */

andl %esp,%ecx

movl %ecx,p

这三条指令执行后,p就是在执行指令的CPU上运行的当前进程的thread_info结构的指针。不过,进程最经常使用的是进程描述符的地址,而不是thread_info结构的地址。为了得到当前在CPU上运行进程的描述符指针,内核要调用current宏,该宏本质上等价于current_thread_info( )->task,它产生以下汇编指令:

movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */

andl %esp,%ecx

movl (%ecx),p

由于task字段在thread_info结构中的偏移量为0,因此执行完这三条指令以后,p就是CPU上运行进程的描述符指针。

current宏常常做为进程描述符字段的前缀出如今内核代码中,例如,current->pid返回在CPU上正在执行CPU的进程的PID。

4)进程链表

Linux内核把进程链表把全部进程的描述符连接起来。每一个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后面的的task_struct元素。

进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。

SET_LINKS 和 REMOVE_LINKS 宏分别用于从进程链表中插入和删除一个进程描述符。这些宏考虑了进程间的父子关系。

另外,还有一个颇有用的宏就是for_each_process,它的功能是扫描整个进程链表,其定义以下:

#define for_each_process(p) /

for (p=&init_task; (p=list_entry((p)->tasks.next, /

struct task_struct, tasks) /

) != &init_task; )

5)state字段

进程描述符task_struct结构的state字段描述了进程当前所处的状态。它由一组标志组成,其中每一个标志描述一种可能的进程状态。在当前的Linux版本中,这些状态是互斥的,所以,严格意义上来讲,只能设置一种状态,其他的标志位将被清除。下面是可能的状态:

可运行状态(TASK_RUNNING)

进程要么在CPU上执行,要么准备执行。

可中断的等待状态(TASK_INTERRUPTIBLE)

进程被挂起(睡眠),直到某个条件变为真。产生一个硬件中断、释放进程正在等待的系统资源、或传递一个信号都是能够唤醒进程的条件(把进程状态放回到TASK_RUNNING)。

不可中断的等待状态(TASK_UNINTERRUPTIBLE)

与可中断的等待状态相似,但有一个例外,把信号传递到该睡眠进程时,不能改变它的状态。这种状态不多用到,但在一些特定条件下(进程必须等待,直到一个不能被中断的时事件发生),这种状态是颇有用的。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时会用到这种状态。探测完成之前,设备驱动程序不能被中断,不然,硬件设备会处于不可预知的状态。

暂停状态(TASK_STOPPED)

进程的执行被暂停。当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后,进人暂停状态。

跟踪状态(TASK_TRACED)

进程的执行已由debugger程序暂停。当一个进程被另外一个进程监控时(例如debugger执行ptrace()系统调用监控一个测试程序)任何信号均可以把这个进程置于TASK_TRACED状态。

还有两个进程状态既能够存放在进程描述符的state字段啊中,也能够存放在exit_state中字段中。从这两个字段的名称能够看出,只有当进程的执行被终止时,进程的状态才会变成此两种中的一种:

僵死状态(EXIT_ZOMBIE)

进程的执行被终止,可是父进程还没发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据,由于父进程可能还须要它。

僵死撤销状态(EXIT_DEAD)

终状态:因为父进程刚发出wait4()或waitpid()系统调用,于是进程由系统删除。为了防止其余执行线程在同一个进程上也执行wait()类系统调用(这也是一种竞争条件),而把进程的状态由僵死(EXIT_ZOMBIE)状态改成僵死撤销状态(EXIT_DEAD)

state字段的值一般用一个简单的赋值语句设置,例如:

p->state = TASK_RUNNING;

内核也使用set_task_state和set_current_state宏:它们分别设置指定进程的状态和当前执行进程的状态。此外,这些宏确保编译程序或CPU控制单元不把赋值操做和其余指令混合。混合指令的顺序有时会致使灾难性的后果。

6)TASK_RUNNING状态的进程链表

当内核寻找到一个新进程在CPU上运行时,必须只考虑可运行进程(即处在TASK_RUNNING状态的进程)。

早先的Linux版本把全部的可运行进程都放在同一个叫作运行队列(runqueue)的链表中,因为维持链表中的进程优先级排序的开销过大,所以,早期的调度程序不得不为选择“最佳”可运行进程而扫描整个队列。

Linux 2.6实现的运行队列有所不一样。其目的是让调度程序能在固定的时间内选出“最佳”可运行队列,与进程中可运行的进程数无关。

提升调度程序运行速度的诀窍是创建多个可运行进程链表,每种进程优先级对应一个不一样的链表。每一个task_struct描述符包含一个list_head类型的字段run_list。若是进程的优先权等于k(其取值范围从0到139),run_list字段就把该进程的优先级链入优先级为k的可运行进程的链表中。此外,在多处理器系统中,每一个CPU都有它本身的运行队列,即它本身的进程链表集。这是一个经过使数据结构更复杂来改善性能的典型例子:调度程序的操做效率的确更高了,但运行队列的链表却为此被拆分红140个不一样的队列!

内核必须为系统中每一个运行队列保存大量的数据,不过运行队列的主要数据结构仍是组成运行队列的进程描述符链表,全部这些链表都由一个单独的prio_array_t数据结构来实现。

enqueue_task(p,array)函数把进程描述符(p参数)插入到某个运行队列的链表(基于prio_array_t结构的array参数),其代码本质上等同于以下代码:

list_add_tail(&p->run_list, &array->queue[p->prio]);

__set_bit(p->prio, array->bitmap);

array->nr_active++;

p->array = array;

进程描述符的prio字段存放进程的动态优先权,而array字段是一个指针,指向当前运行队列的proo_array_t数据结构。相似地,dequeue_task(p,array)函数从运行队列的链表中删除一个进程的描述符。

7)进程间关系

父子兄弟关系:

程序建立的进程具备父/子关系。若是一个进程建立多个子进程时,则子进程之间具备兄弟关系。进程0和进程1是由内核建立的;进程1(init)是全部进程的祖先。

在进程描述符中引入几个字段来表示这些关系,咱们假设拥有该task_struct结构的这个进程叫P:

real_parent——指向建立了P进程的描述符,若是进程P的父进程不存在,就指向进程1的描述符(所以,若是用户运行了一个后台进程并且退出了shell,后台进程就会变成init的子进程)。

parent——指向P的当前父进程(这种进程的子进程终止时,必须向父进程发信号)。它的值一般与reak_parent一致,但偶尔也能够不一样,例如,当另外一个进程发出监控P的ptrace系统调用请求时。

children——链表的头部,链表中全部的元素都是P建立的子进程。

sibling——指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程跟P是同样的。

下图显示了一组进程间的亲属关系,进程P0建立了P1,P2,P3,进程P3又建立了P4。

其余关系:此外,进程之间还存在其余关系:一个进程多是一个进程组或登陆会话的领头进程,也多是一个线程组的领头进程,他还可能跟踪其余进程的执行,下面就列出进程描述符中的一些字段,这些字段创建起了进程P和其余进程之间的关系:

group_leader——P所在进程组的领头进程的描述符指针

signal->pgrp——P所在进程组的领头进程的PID

tgid——P所在线程组的领头进程的PID

signal->session——P的登陆会话领头进程的PID

ptrace_children——链表的头,该链表包含全部被debugger程序跟踪的P的子进程

ptrace_list——指向所跟踪进程其实际父进程链表的前一个和下一个元素(用于P被跟踪的时候)

8)PID定位task_struct

再来,内核必须能从进程的PID导出对应的进程描述符指针。例如,为kill()系统调用提供服务时就会发生这种状况:当进程P1但愿向另外一个进程P2发送一个信号时,P1调用kill()系统调用,其参数为P2的PID,内核从这个PID导出其对应的进程描述符,而后从该task_struct中取出记录挂起信号的数据结构指针。

那么如何获得这个task_struct呢?首先想到for_each_process(p)。不行,虽然顺序扫描进程链表并检查进程描述符的pid字段是可行的,但至关低效。为了加速查找,Linux内核引入了4个散列表。须要4个散列表是由于进程描述符包含了表示不一样类型PID的字段,并且每种类型的PID须要它本身的散列表:

PIDTYPE_PID    pid    进程的PID

PIDTYPE_TGID    tgid    线程组领头进程的PID

PIDTYPE_PGID    pgrp    进程组领头进程的PID

PIDTYPE_SID    session    会话领头的PID

内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。一个散列表的长度依赖于可用的RAM的容量,例如:一个系统拥有512MB的RAM,那么每一个散列表就被存在4个页框中,可拥有2048个表项。

用pid_hashfn宏把PID转化为表索引:

#define pid_hashfn(x) hash_long((unsigned long) x, pidhash_shift)

变量pidhash_shift用来存放表索引的长度(以位为单位的长度,在咱们这里是11位)。不少散列函数都使用hash_long(),在32位体系结构中它基本等价于:

unsigned long hash_long(unsigned long val, unsigned int bits)

{

unsigned long hash = val * 0x9e370001UL;

return hash >> (32 - bits);

}

由于咱们这里的pidhash_shift等于11,因此pid_hashfn的取值范围是0到2^11 - 1=2047。

正如计算机科学的基础课程所阐述的那样,散列函数并不总能确保PID与表的索引一一对应。两个不一样的PID散列到相同的表索引称为冲突(colliding)。Linux利用链表来处理冲突的PID:每一个表项是由冲突的进程描述符组成的双向循环链表

(二)进程调度

1)进程调度的目标

1.高效性:高效意味着在相同的时间下要完成更多的任务,调度程序会被频繁的执行,因此调度程序要尽量高效。

2.增强交互性能:在系统至关的负载下,也要保证系统的响应时间

3.保证公平和避免饥渴

4.SMP调度:调度程序必须支持多处理系统

5.软实时调度:系统必须有效的调用实时进程,但不保证必定知足其要求。

2)进程优先级

进程提供了两种优先级,一种是普通的进程优先级,一种是实时进程优先级。

前者适用SCHED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_RR调度策略,任什么时候候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照FIFO(一次机会作完)或者RR(屡次轮转)规则调度的。

实时进程,只有静态优先级,由于内核不会再根据休眠等因素对其静态优先级作调整,其范围在0~MAX_RT_PRIO-1间。默认MAX_RT_PRIO配置为100,也即,默认的实时优先级范围是0~99。而nice值,影响的是优先级在MAX_RT_PRIO~MAX_RT_PRIO+40范围内的进程。

不一样与普通进程,系统调度时,实时优先级高的进程老是先于优先级低的进程执行,直到实时优先级高的实时进程没法执行。实时进程老是被认为处于活动状态。若是有数个 优先级相同的实时进程,那么系统就会按照进程出如今队列上的顺序选择进程,假设当前CPU运行的实时进程A的优先级为a,而此时有个优先级为b的实时进程B进入可运行状态,那么只要b

不一样调度策略的实时进程只有在相同优先级时才有可比性:

1. 对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其余进程执行。因而可知至关霸道。

2. 对于RR的进程。一旦时间片消耗完毕,则会将该进程置于队列的末尾,而后运行其余相同优先级的进程,若是没有其余相同优先级的进程,则该进程会继续执行。

总而言之,对于实时进程,高优先级的进程就是大爷。它执行到无法执行了,才轮到低优先级的进程执行。

普通进程的调度

Linux对于普通的进程,根据动态优先级进行调度,而动态优先级是由静态优先级调整而来,Linux下,静态优先级是用户不可见的,隐藏在内核中,而内核提供给用户一个能够影响静态优先级的接口,那就是nice值。

关系以下:

static_prio =MAX_RT_PRIO+nice+20

nice值的范围是-20~19,于是静态优先级范围在100~139之间,nice数值越大就使得static_prio越大,最终进程优先级就越低。

咱们前面也说了,系统调度时,还会考虑其余因素,于是会计算出一个叫进程动态优先级的东西,根据此来实施调度。由于,不只要考虑静态优先级,也要考虑进程

的属性。例如若是进程属于交互式进程,那么能够适当的调高它的优先级,使得界面反应地更加迅速,从而使用户获得更好的体验。Linux2.6

在这方面有了较大的提升。Linux2.6认为,交互式进程能够从平均睡眠时间这样一个measurement进行判断。进程过去的睡眠时间越多,则越有

可能属于交互式进程。则系统调度时,会给该进程更多的奖励(bonus),以便该进程有更多的机会可以执行。奖励(bonus)从0到10不等。

系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入非运行状态,或者时间片消耗完毕才会轮到动态优先级较低的进程执行。动态优先级的计算主要考虑两个因素:静态优先级,进程的平均睡眠时间也即bonus。计算公式以下,

dynamic_prio = max (100, min (static_prio - bonus + 5, 139))

为何根据睡眠和运行时间肯定奖惩分数是合理的

睡眠和CPU耗时反应了进程IO密集和CPU密集两大瞬时特色,不一样时期,一个进程可能便是CPU密集型也是IO密集型进程。对于表现为IO密集的进程,应该常常运行,但每次时间片不要太长。对于表现为CPU密集的进程,CPU不该该让其常常运行,但每次运行时间片要长。交互进程为例,假如以前其其大部分时间在于等待CPU,这时为了调高相应速度,就须要增长奖励分。另外一方面,若是此进程老是耗尽每次分配给它的时间片,为了对其余进程公平,就要增长这个进程的惩罚分数。能够参考CFS的virtutime机制.

3)现代方法CFS

再也不单纯依靠进程优先级绝对值,而是参考其绝对值,综合考虑全部进程的时间,给出当前调度时间单位内其应有的权重,也就是,每一个进程的权重X单位时间=应获cpu时间,可是这个应得的cpu时间不该过小(假设阈值为1ms),不然会由于切换得不偿失。可是,当进程足够多时候,确定有不少不一样权重的进程得到相

同的时间——最低阈值1ms,因此,CFS只是近似彻底公平。

4)Linux进程状态机

进程是经过fork系列的系统调用(fork clone,vfork)来建立的,内核,内核模块也能够经过kernel_thread函数建立内核进程,这些建立子进程的函数本质上都完成了相同的功能——将调用进程复制一份,获得子进程。(能够经过选项参数来决定各类资源是共享、仍是私有。)那么既然调用进程处于TASK_RUNNING状态(不然,它若不是正在运行,又怎么进行调用?),则子进程默认也处于TASK_RUNNING状态。

另外,在系统调用clone和内核函数kernel_thread也接受CLONE_STOPPED选项,从而将子进程的初始状态置为 TASK_STOPPED。

进程建立后,状态可能发生一系列的变化,直到进程退出。而尽管进程状态有好几种,可是进程状态的变迁却只有两个方向——从TASK_RUNNING状态变为非TASK_RUNNING状态、或者从非TASK_RUNNING状态变为TASK_RUNNING状态。总之,TASK_RUNNING是必经之路,不可能两个非RUN状态直接转换。

也就是说,若是给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNING状态),而后再响应SIGKILL信号而退出(变为TASK_DEAD状态)。并不会从TASK_INTERRUPTIBLE状态直接退出。

进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也多是中断处理程序)执行唤醒操做来实现的。执行唤醒的

进程设置被唤醒进程的状态为TASK_RUNNING,而后将其task_struct结构加入到某个CPU的可执行队列中。因而被唤醒的进程将有机会被

调度执行。

而进程从TASK_RUNNING状态变为非TASK_RUNNING状态,则有两种途径:

一、响应信号而进入TASK_STOPED状态、或TASK_DEAD状态;

二、执行系统调用主动进入TASK_INTERRUPTIBLE状态(如nanosleep系统调用)、或TASK_DEAD状态(如exit系统调用);或因为执行系统调用须要的资源得不到满     足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。

显然,这两种状况都只能发生在进程正在CPU上执行的状况下。

经过ps命令咱们可以查看到系统中存在的进程,以及它们的状态:R(TASK_RUNNING),可执行状态。

只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出如今一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。

只要可执行队列不为空,其对应的CPU就不能偷懒,就要执行其中某个进程。通常称此时的CPU“忙碌”。对应的,CPU“空闲”就是指其对应的可执行队列为空,以至于CPU无事可作。

有人问,为何死循环程序会致使CPU占用高呢?由于死循环程序基本上老是处于TASK_RUNNING状态(进程处于可执行队列中)。除非一些很是极端状况(好比系统内存严重紧缺,致使进程的某些须要使用的页面被换出,而且在页面须要换入时又没法分配到内存……),不然这个进程不会睡眠。因此CPU的可执行队列老是不为空(至少有这么个进程存在),CPU也就不会“空闲”。

不少操做系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行可是还没有被调度执行的进程定义为READY状态,这两种状态在linux下统一为 TASK_RUNNING状态。

S(TASK_INTERRUPTIBLE),可中断的睡眠状态。

处于这个状态的进程由于等待某某事件的发生(好比等待socket链接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其余进程触发),对应的等待队列中的一个或多个进程将被唤醒。

经过ps命令咱们会看到,通常状况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,若是不是绝大多数进程都在睡眠,CPU又怎么响应得过来。

D(TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。

与TASK_INTERRUPTIBLE状态相似,进程处于睡眠状态,可是此刻进程是不可中断的。不可中断,指的并非CPU不响应外部硬件的中断,而是指进程不响应异步信号。

绝大多数状况下,进程处在睡眠状态时,老是应该可以响应异步信号的。不然你将惊奇的发现,kill -9居然杀不死一个正在睡眠的进程了!因而咱们也很好理解,为何ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而老是TASK_INTERRUPTIBLE状态。

而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。若是响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),因而原有的流程就被中断了(参见《linux异步信号handle浅析》)。

在进程对某些硬件进行操做时(好比进程调用read系统调用对某个设备文件进行读操做,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能须要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以免进程与设备交互的过程被打断,形成设备陷入不可控的状态。(好比read系统调用触发了一次磁盘到用户空间的内存的DMA,若是DMA进行过程当中,进程因为响应信号而退出了,那么DMA正在访问的内存可能就要被释放了。)这种状况下的TASK_UNINTERRUPTIBLE状态老是很是短暂的,经过ps命令基本上不可能捕捉到。

linux系统中也存在容易捕捉的TASK_UNINTERRUPTIBLE状态。执行vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用exit或exec。

经过下面的代码就能获得处于TASK_UNINTERRUPTIBLE状态的进程:

#include

void main() {

if (!vfork()) sleep(100);

}

向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程自己处于TASK_UNINTERRUPTIBLE状态而不响应信号)。(SIGSTOP与SIGKILL信号同样,是很是强制的。不容许用户进程经过signal系列的系统调用从新设置对应的信号处理函数。)

向进程发送一个SIGCONT信号,可让其从TASK_STOPPED状态恢复到TASK_RUNNING状态。

当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操做。好比在gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。而在其余时候,被跟踪的进程仍是处于前面提到的那些状态。

对于进程自己来讲,TASK_STOPPED和TASK_TRACED状态很相似,都是表示进程暂停下来。

而TASK_TRACED状态至关于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程经过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH等操做(经过ptrace系统调用的参数指定操做),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。

Z(TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。

进程在退出的过程当中,处于TASK_DEAD状态。

在这个退出过程当中,进程占有的全部资源将被回收,除了task_struct结构(以及少数资源)之外。因而进程就只剩下task_struct这么个空壳,故称为僵尸。

之因此保留task_struct,是由于task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程极可能会关心这些信息。好比在shell中,$?变量就保存了最后一个退出的前台进程的退出码,而这个退出码每每被做为if语句的判断条件。

固然,内核也能够将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。可是使用task_struct结构更为方便,由于在内核中已经创建了从pid到task_struct查找关系,还有进程间的父子关系。释放掉task_struct,则须要创建一些新的数据结构,以便让父进程找到它的子进程的退出信息。

父进程能够经过wait系列的系统调用(如wait四、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。而后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。

子进程在退出的过程当中,内核会给其父进程发送一个信号,通知父进程来“收尸”。这个信号默认是SIGCHLD,可是在经过clone系统调用建立子进程时,能够设置这个信号。

经过下面的代码可以制造一个EXIT_ZOMBIE状态的进程:

#include

void main() {

if (fork())

while(1) sleep(100);

}

编译运行,而后ps一下:

kouu@kouu-one:~/test$ ps -ax | grep a\.out

10410 pts/0      S+       0:00 ./a.out

10411 pts/0      Z+       0:00 [a.out]

10413 pts/1      S+       0:00 grep a.out

只要父进程不退出,这个僵尸状态的子进程就一直存在。那么若是父进程退出了呢,谁又来给子进程“收尸”?

当进程退出的时候,会将它的全部子进程都托管给别的进程(使之成为别的进程的子进程)。托管给谁呢?多是退出进程所在进程组的下一个进程(若是存在的话),或者是1号进程。因此每一个进程、每时每刻都有父进程存在。除非它是1号进程。

1号进程,pid为1的进程,又称init进程。

linux系统启动后,第一个被建立的用户态进程就是init进程。它有两项使命:

一、执行系统初始化脚本,建立一系列的进程(它们都是init进程的子孙);

二、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工做;

init进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程当中处于TASK_INTERRUPTIBLE状态,“收尸”过程当中则处于TASK_RUNNING状态。

X(TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁。

而进程在退出过程当中也可能不会保留它的task_struct。好比这个进程是多线程程序中被detach过的进程(进程?线程?参见《linux线程浅析》)。或者父进程经过设置SIGCHLD信号的handler为SIG_IGN,显式的忽略了SIGCHLD信号。(这是posix的规定,尽管子进程的退出信号能够被设置为SIGCHLD之外的其余信号。)

此时,进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码当即就会将该进程完全释放。因此EXIT_DEAD状态是很是短暂的,几乎不可能经过ps命令捕捉到。

5)调度触发的时机

调度的触发主要有以下几种状况:

一、当前进程(正在CPU上运行的进程)状态变为非可执行状态。

进程执行系统调用主动变为非可执行状态。好比执行nanosleep进入睡眠、执行exit退出、等等;

进程请求的资源得不到知足而被迫进入睡眠状态。好比执行read系统调用时,磁盘高速缓存里没有所须要的数据,从而睡眠等待磁盘IO;

进程响应信号而变为非可执行状态。好比响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;

二、抢占。进程运行时,非预期地被剥夺CPU的使用权。这又分两种状况:进程用完了时间片、或出现了优先级更高的进程。

优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒,或由于释放互斥对象(如释放锁)而被唤醒;

内核在响应时钟中断的过程当中,发现当前进程的时间片用完;

内核在响应中断的过程当中,发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒。好比CPU收到网卡中断,内核处理该中断,发现某个socket可读,因而唤醒正在等待读这个socket的进程;再好比内核在处理时钟中断的过程当中,触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程;

6)内核抢占

理想状况下,只要知足“出现了优先级更高的进程”这个条件,当前进程就应该被马上抢占。可是,就像多线程程序须要用锁来保护临界区资源同样,内核中也存在不少这样的临界区,不大可能随时随地都能接收抢占。

linux 2.4时的设计就很是简单,内核不支持抢占。进程运行在内核态时(好比正在执行系统调用、正处于异常处理函数中),是不容许抢占的。必须等到返回用户态时才会触发调度(确切的说,是在返回用户态以前,内核会专门检查一下是否须要调度);

linux 2.6则实现了内核抢占,可是在不少地方仍是为了保护临界区资源而须要临时性的禁用内核抢占。

也有一些地方是出于效率考虑而禁用抢占,比较典型的是spin_lock。spin_lock是这样一种锁,若是请求加锁得不到知足(锁已被别的进程占有),则当前进程在一个死循环中不断检测锁的状态,直到锁被释放。

为何要这样忙等待呢?由于临界区很小,好比只保护“i+=j++;”这么一句。若是由于加锁失败而造成“睡眠-唤醒”这么个过程,就有些得不偿失了。

那么既然当前进程忙等待(不睡眠),谁又来释放锁呢?其实已获得锁的进程是运行在另外一个CPU上的,而且是禁用了内核抢占的。这个进程不会被其余进程抢占,因此等待锁的进程只有可能运行在别的CPU上。(若是只有一个CPU呢?那么就不可能存在等待锁的进程了。)

而若是不由用内核抢占呢?那么获得锁的进程将可能被抢占,因而可能好久都不会释放锁。因而,等待锁的进程可能就不知何年何月得偿所望了。

对于一些实时性要求更高的系统,则不能容忍spin_lock这样的东西。宁肯改用更费劲的“睡眠-唤醒”过程,也不能由于禁用抢占而让更高优先级的进程等待。好比,嵌入式实时linux montavista就是这么干的。

因而可知,实时并不表明高效。不少时候为了实现“实时”,仍是须要对性能作必定让步的。

7)多处理器下的负载均衡

前面咱们并无专门讨论多处理器对调度程序的影响,其实也没有什么特别的,就是在同一时刻能有多个进程并行地运行而已。那么,为何会有“多处理器负载均衡”这个事情呢?

若是系统中只有一个可执行队列,哪一个CPU空闲了就去队列中找一个最合适的进程来执行。这样不是很好很均衡吗?

的确如此,可是多处理器共用一个可执行队列会有一些问题。显然,每一个CPU在执行调度程序时都须要把队列锁起来,这会使得调度程序难以并行,可能致使系统性能降低。而若是每一个CPU对应一个可执行队列则不存在这样的问题。

另外,多个可执行队列还有一个好处。这使得一个进程在一段时间内老是在同一个CPU上执行,那么极可能这个CPU的各级cache中都缓存着这个进程的数据,颇有利于系统性能的提高。

因此,在linux下,每一个CPU都有着对应的可执行队列,而一个可执行状态的进程在同一时刻只能处于一个可执行队列中。

因而,“多处理器负载均衡”这个麻烦事情就来了。内核须要关注各个CPU可执行队列中的进程数目,在数目不均衡时作出适当调整。何时须要调整,以多大力度进程调整,这些都是内核须要关心的。固然,尽可能不要调整最好,毕竟调整起来又要耗CPU、又要锁可执行队列,代价仍是不小的。

另外,内核还得关心各个CPU的关系。两个CPU之间,多是相互独立的、多是共享cache的、甚至多是由同一个物理CPU经过超线程技术虚拟出来的……CPU之间的关系也是实现负载均衡的重要依据。关系越紧密,进程在它们之间迁移的代价就越小。参见《linux内核SMP负载均衡浅析》。

优先级继承

因为互斥,一个进程(设为A)可能由于等待进入临界区而睡眠。直到正在占有相应资源的进程(设为B)退出临界区,进程A才被唤醒。

可能存在这样的状况:A的优先级很是高,B的优先级很是低。B进入了临界区,可是却被其余优先级较高的进程(设为C)抢占了,而得不到运行,也就没法退出临界区。因而A也就没法被唤醒。

A有着很高的优先级,可是如今却沦落到跟B一块儿,被优先级并不过高的C抢占,致使执行被推迟。这种现象就叫作优先级反转。

出现这种现象是很不合理的。较好的应对措施是:当A开始等待B退出临界区时,B临时获得A的优先级(仍是假设A的优先级高于B),以便顺利完成处理过程,退出临界区。以后B的优先级恢复。这就是优先级继承的方法。

中断处理线程化

在linux下,中断处理程序运行于一个不可调度的上下文中。从CPU响应硬件中断自动跳转到内核设定的中断处理程序去执行,到中断处理程序退出,整个过程是不能被抢占的。

一个进程若是被抢占了,能够经过保存在它的进程控制块(task_struct)中的信息,在以后的某个时间恢复它的运行。而中断上下文则没有task_struct,被抢占了就无法恢复了。

中断处理程序不能被抢占,也就意味着中断处理程序的“优先级”比任何进程都高(必须等中断处理程序完成了,进程才能被执行)。可是在实际的应用场景中,可能某些实时进程应该获得比中断处理程序更高的优先级。

因而,一些实时性要求更高的系统就给中断处理程序赋予了task_struct以及优先级,使得它们在必要的时候可以被高优先级的进程抢占。可是显然,作这些工做是会给系统形成必定开销的,这也是为了实现“实时”而对性能作出的一种让步。

(三)进程同步与互斥

多进程系统中避免不了进程之间的相互关系,最主要是两种关系--同步和互斥。

进程同步 是进程间直接的相互做用,是合做进程间的有意识的行为。咱们也要有必定的同步机制保证它们的执行次序。

进程互斥是进程之间发生的一种间接性做用,通常是程序不但愿的。一般的状况是两个或两个以上的进程须要同时访问某个共享变量。咱们通常将发生可以问共享变量的程序段称为临界区。两个进程不能同时进入临界区,不然就会致使数据的不一致,产生与时间有关的错误。解决互斥问题应该知足互斥和公平两个原则,即任意时刻只能容许一个进程处于同一共享变量的临界区,并且不能让任一进程无限期地等待。互斥问题能够用硬件方法解决,也能够用软件方法。

同步是说进程的合做关系,互斥是说进程对资源的竞争关系。

信号量、管程

二,管程:参考自http://hi.baidu.com/zucenaa/blog/item/e63d22277c9d9c09918f9de2.html

信号量机制功能强大,但使用时对信号量的操做分散,并且难以控制,读写和维护都很困难。所以后

来又提出了一种集中式同步进程——管程。其基本思想是将共享变量和对它们的操做集中在一个模块中,操做系统或并发程序就由这样的模块构成。这样模块之间联

系清晰,便于维护和修改,易于保证正确性。

管程做为一个模块,它的类型定义以下:

monitor_name = MoNITOR;

共享变量说明;

define 本管程内部定义、外部可调用的函数名表;

use 本管程外部定义、内部可调用的函数名表;

内部定义的函数说明和函数体

{

共享变量初始化语句;

}

从语言的角度看,管程主要有如下特性:

(1)模块化。管程是一个基本程序单位,能够单独编译;

(2)抽象数据类型。管程是中不只有数据,并且有对数据的操做;

(3)信息掩蔽。管程外能够调用管程内部定义的一些函数,但函数的具体实现外部不可见;

对于管程中定义的共享变量的全部操做都局限在管程中,外部只能经过调用管程的某些函数来间接访问这些变量。所以管程有很好的封装性。

为了保证共享变量的数据一致性,管程应互斥使用。 管程一般是用于管理资源的,所以管程中有进程等待队列和相应的等待和唤醒操做。在管程入口有一个等待队列,称为入口等待队列。当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另外一个进程时,二者必须有一个退出或中止使用管程。在管程内部,因为执行唤醒操做,可能存在多个等待进程(等待使用管程),称为紧急等待队列,它的优先级高于入口等待队列。

所以,一个进程进入管程以前要先申请,通常由管程提供一个enter过程;离开时释放使用权,若是紧急等待队列不空,则唤醒第一个等待者,通常也由管程提供外部过程leave。

管程内部有本身的等待机制。管程能够说明一种特殊的条件型变量:var c:condition;其实是一个指针,指向一个等待该条件的PCB队列。对条件型变量可执行wait和signal操做:(联系P和V; take和give)

wait(c):若紧急等待队列不空,唤醒第一个等待者,不然释放管程使用权。执行本操做的进程进入C队列尾部;

signal(c):若C队列为空,继续原进程,不然唤醒队列第一个等待者,本身进入紧急等待队列尾部。

(四)进程间通讯(IPC)

进程间通讯主要包括 管道,系统IPC(包括消息队列,信号量,共享内存), SOCKET.

管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通讯,而有名管道则可用于无亲属关系的进程之间。

消息队列用于运行于同一台机器上的进程间通讯,与管道类似;

消息队列用于运行于同一台机器上的进程间通讯,与管道类似;

共享内存一般由一个进程建立,其他进程对这块内存区进行读写。获得共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不经常使用,由于它控制存取的是实际的物理内存;

本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取情况。通常说来,为了得到共享资源,进程须要执行下列操做:

(1)测试控制该资源的信号量;

(2)若此信号量的值为正,则容许进行使用该资源,进程将进号量减1;

(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);

(4)当进程再也不使用一个信号量控制的资源时,信号量值加1,若是此时有进程正在睡眠等待此信号量,则唤醒此进程。

套接字通讯并不为Linux所专有,在全部提供了TCP/IP协议栈的操做系统中几乎都提供了socket,而全部这样操做系统,对套接字的编程方法几乎是彻底同样的。

管道:速度慢,容量有限,只有父子进程能通信

FIFO(命名管道):任何进程间都能通信,但速度慢,命名管道可用于非父子进程,命名管道就是FIFO,管道是先进先出的通信方式

消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

信号量:不能传递复杂消息,只能用来同步

共享内存区:可以很容易控制容量,速度快,但要保持同步,好比一个进程在写的时候,另外一个进程要注意读写的问题,至关于线程中的线程安全,固然,共享内存区一样能够用做线程间通信,不过没这个必要,线程间原本就已经共享了同一进程内的一块内存。

线程

线程是CPU调度的最小单位,多个线程共享一个进程的地址空间。

线程包含线程ID,程序计数器,寄存器和栈。

做者:简书的王布斯 连接:https://www.jianshu.com/p/7ce30a806c51 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
相关文章
相关标签/搜索