一.进程的概念
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
其中在Linux内核中赋予它更通用的名称----任务(task)
进程在整个内核中的功能位置:
我们还可以分两个层次对操作系统进程进行讨论。
在较高的层次上,“进程”是一个重要的组织概念,用其说明一个计算机系统作为一个整体的活动。将计算机系统视作若干进程的组合活动是适合的,每一个进程与一道特定的程序相结合。例如“shell”或者“vi”编辑程序。在这一层次上,进程本身被视作系统中的活动实体,而真正的活动部件本体,即处理机和外部设备则被消隐,不引起人们的注意。进程诞生、生长,然后死亡;它们存在的数量在不断变化;它们可以获得并释放资源;它们可以交互作用、合作、冲突、共享资源等等。
在较低的层次上,进程是不活动的实体,它们依靠活动的实体,例如处理机才起作用。借助于频繁地使用处理机从一个进程映像的执行切换到另一个,就可以产生一种印象:每一个进程映像都连续发生变化,这就导致较高层次上的解释。
Linux进程的四个要素:
1.有一段程序供其执行,这段程序不一定是某个进程所专有的,可以与其他进程共用。
2.有进程专用的内核空间堆栈。
3.在内核中有一个task_struct数据结构,即通常所说的“进程控制块”。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。同时,这个结构还记录着进程所占用的各项资源。
4.有独立的存储空间,这意味着拥有专有的用户空间;进一步,还意味着除前述的内核空间堆栈外还有其专用的用户空间堆栈。有一点必须指出,内核空间是不能独立的,任何进程都不可能直接(不通过系统调用)改变内核空间的内容(除其本身的内核空间堆栈以外)。
二.进程的组织:
进程控制块
进程创建时,操作系统就新建一个PCB结构,它之后就常驻内存,任一时刻可以存取, 在进程结束时删除。PCB是进程实体的一部分,是进程存在的唯一标志。当创建一个进程时,系统为该进程建立一个PCB;当进程执行时,系统通过其PCB 了 解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其PCB,该进 程随之消亡。操作系统通过PCB表来管理和控制进程。
进程描述信息 | 进程控制和管理信息 | 资源分配清单 | 处理机相关信息 |
---|---|---|---|
进程标识符(PID) | 进程当前状态 | 代码段指针 | 通用寄存器值 |
用户标识符(UID) | 进程优先级 | 数据段指针 | 地址寄存器值 |
代码运行入口地址 | 堆栈段指针 | 控制寄存器值 | |
程序的外存地址 | 文件描述符 | 标志寄存器值 | |
进入内存时间 | 键盘 | 状态字 | |
处理机占用时间 | 鼠标 | ||
信号量使用 |
表2-1是一个PCB的实例,PCB主要包括进程描述信息、进程控制和管理信息、资源 分配清单和处理机相关信息等。各部分的主要说明如下:
1) 进程描述信息
进程标识符:标志各个进程,每个进程都有一个并且是唯一的标识号。
用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。
2) 进程控制和管理信息
进程当前状态:描述进程的状态信息,作为处理机分配调度的依据。
进程优先级:描述进程抢占处理机的优先级,优先级高的进程可以优先获得处理机。
3) 资源分配清单,用于说明有关内存地址空间或虚拟地址空间的状况;所打开文件的 列表和所使用的输入/输出设备信息。
4) 处理机相关信息,主要指处理机中各寄存器值,当进程被切换时,处理机状态信息 都必须保存在相应的PCB中,以便在该进程重新执行时,能再从断点继续执行。
在一个系统中,通常存在着许多进程,有的处于就绪状态,有的处于阻塞状态,而且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的PCB用适当的方法组织起来。目前,常用的组织方式有链接方式和索引方式两种。链接方式将同一状态的PCB链接成一个队列,不同状态对应不同的队列,也可以把处于阻塞状态的进程的PCB,根据其阻塞原因的不同,排成多个阻塞队列。索引方式是将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB,不同状态对应不同的索引表,如就绪索引表和阻塞索引表等。
/* wq为某个等待队列的队列头 */ void sleep_on (wait_queue_head_t *wq) { /* 声明一个等待队列结点 */ wait_queue_t wait; /* 用当前进程初始化这个等待队列结点 */ init_waitqueue_entry (&wait, current); /* 设置当前进程状态为TASK_UNINTERRUPTIBLE */ current->state = TASK_UNINTERRUPTIBLE; /* 将这个代表着当前进程的等待队列结点加入到wq这个等待队列 */ add_wait_queue (wq, &wait); /* 请求调度器进行调度,执行完schedule后进程会被移除CPU运行队列,只有等待队列唤醒后才会重新回到CPU运行队列 */ schedule (); /* 这里进程已经被等待队列唤醒,重新移到CPU运行队列,也就是等待的条件已经为真,唤醒后第一件事就是将自己从等待队列wq中移除 */ remove_wait_queue (wq, &wait); }
int do_fork(unsigned long clone_flags,unsigned long usp,struct pt_regs *regs) { 取一个空闲的task数组表项和唯一的PID号; 根据clone_flags参数的值将父进程的进程表现拷贝到子进程表项中或设置为共享; 把进程加入进程图表设置跟踪进程的数据结构 调用hash_pid把新进程置入pidhash表中; 调用wake_up_process设进程为TASK_RUNNING并置入运行队列; return(p->pid) }
在创建新进程后,我们需要它来处理其他实际的工作,通过调用exec来执行别的实际程序,就能够变成独立于其他进程的进程了,因此,创建一个真正的进程--与其祖先不同的程序镜像,要分为两步,一步是fork,另一步是exec.下面是C代码描述:
/*实验fork和exec*/ if((result=fork()==0) { /*child code*/ if(exec( "new_program")<0) perror("exec failed"); exit(1); } else if(result<0) { perror ("fork failde"); }
三.进程状态转换
在一个给定时刻内,进程处于下面六种状态之一,进程的当前状态被记录在struct task_struct结构中的state成员中
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ …… };
在/include/linux/sched.h定义的进程状态:
#define TASK_RUNNING 0 /* 进程准备好运行 */ #define TASK_INTERRUPTIBLE 1 /* 等待特定事件,可以被信号中断 */ #define TASK_UNINTERRUPTIBLE 2 /* 等待特定硬件条件,不可以被信号中断*/ #define TASK_ZOMBIE 4 /* 进程已经退出 */ #define TASK_STOPPED 8 /* 进程已经停止运行 */ #define TASK_SWAPPING 16 /* 进程正在执行磁盘交换工作 */
何时刻一个处理机仅能执行一个进程,而可能不止一个进程处于TASK_RUNNING状态。TASK_RUNNING并不意味着该进程可以立即获得CPU(虽然有时候是这样),而是仅仅说明只要CPU一旦可用,进程就可以立即准备执行了。进程处于TASK_ZOMBIE状态,意味进程已经退出了(或已经被杀掉了),但是其相关的struct task_struct结构并没有被删除。这样即使子进程已经退出,也允许父进程对已经死去的子孙进程进行查询。父进程通过wait来获取TASK_ZOMBIE进程的信息,同时释放它占用的struct task_struct结构。
四.task_struct数据结构
struct task_struct
{
volatile long state; /*state成员的可能取值如下
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_TRACED 8
#define EXIT_DEAD 16 #define EXIT_ZOMBIE 32 #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
#define TASK_DEAD 64 #define TASK_WAKEKILL 128 #define TASK_WAKING 256 #define TASK_PARKED 512 #define TASK_NOLOAD 1024 #define TASK_STATE_MAX 2048 #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)*/
struct list_head run_list;
struct task_struct * next_task,*prev_task;
pid_t pid;
struct task_struct*p_opptr,*P_pptr;/*1.p_opptr指向进程的原始祖先
2.p_pptr指向进程的当前祖先
3.p_cptr指向进程的最年轻子孙
4.p_ysptr指向进程的下一个最年轻兄弟
5.p_osptr指向进程的下一个最古老兄弟*/
*p_cptr,*p_ysptr,*p_osptr;
struct task_struct*pidhash_next;
struct task_struct**pidhash_pprev;
}
进程标识符:
pid_t pid; //进程的标识符 pid_t tgid; //线程组标识符
进程标记符:
unsigned int flags; /* per process flags, defined below */
五.Linux的调度
调度器介绍
- 公平:保证每个进程得到合理的CPU时间。
- 高效:使CPU保持忙碌状态,即总是有进程在CPU上运行。
- 响应时间:使交互用户的响应时间尽可能短。
- 周转时间:使批处理用户等待输出的时间尽可能短。
- 吞吐量:使单位时间内处理的进程数量尽可能多。
- 负载均衡:在多核多处理器系统中提供更高的性能
- 实时进程:对系统的响应时间要求很高,它们需要短的响应时间,并且这个时间的变化非常小,典型的实时进程有音乐播放器,视频播放器等。
- 普通进程:包括交互进程和非交互进程,交互进程如文本编辑器,它会不断的休眠,又不断地通过鼠标键盘进行唤醒,而非交互进程就如后台维护进程,他们对IO,响应时间没有很高的要求,比如编译器。
调度策略
- SCHED_NORMAL:普通进程使用的调度策略,现在此调度策略使用的是CFS调度器。
- SCHED_FIFO:实时进程使用的调度策略,此调度策略的进程一旦使用CPU则一直运行,直到有比其更高优先级的实时进程进入队列,或者其自动放弃CPU,适用于时间性要求比较高,但每次运行时间比较短的进程。
- SCHED_RR:实时进程使用的时间片轮转法策略,实时进程的时间片用完后,调度器将其放到队列末尾,这样每个实时进程都可以执行一段时间。适用于每次运行时间比较长的实时进程
调度
- 调用cond_resched()时
- 显式调用schedule()时
- 从系统调用或者异常中断返回用户空间时
- 从中断上下文返回用户空间
管理组调度,内核引进了struct task_group结构,如下:
/* 进程组,用于实现组调度 */ struct task_group { /* 用于进程找到其所属进程组结构 */ struct cgroup_subsys_state css; #ifdef CONFIG_FAIR_GROUP_SCHED /* CFS调度器的进程组变量,在 alloc_fair_sched_group() 中进程初始化及分配内存 */ /* 该进程组在每个CPU上都有对应的一个调度实体,因为有可能此进程组同时在两个CPU上运行(它的A进程在CPU0上运行,B进程在CPU1上运行) */ struct sched_entity **se; /* 进程组在每个CPU上都有一个CFS运行队列(为什么需要,稍后解释) */ struct cfs_rq **cfs_rq; /* 用于保存优先级默认为NICE 0的优先级 */ unsigned long shares; #ifdef CONFIG_SMP atomic_long_t load_avg; atomic_t runnable_avg; #endif #endif #ifdef CONFIG_RT_GROUP_SCHED /* 实时进程调度器的进程组变量,同 CFS */ struct sched_rt_entity **rt_se; struct rt_rq **rt_rq; struct rt_bandwidth rt_bandwidth; #endif struct rcu_head rcu; /* 用于建立进程链表(属于此调度组的进程链表) */ struct list_head list; /* 指向其上层的进程组,每一层的进程组都是它上一层进程组的运行队列的一个调度实体,在同一层中,进程组和进程被同等对待 */ struct task_group *parent; /* 进程组的兄弟结点链表 */ struct list_head siblings; /* 进程组的儿子结点链表 */ struct list_head children; #ifdef CONFIG_SCHED_AUTOGROUP struct autogroup *autogroup; #endif struct cfs_bandwidth cfs_bandwidth; };
调度实体(struct sched_entity)
1 /* 一个调度实体(红黑树的一个结点),其包含一组或一个指定的进程,包含一个自己的运行队列,一个父亲指针,一个指向需要调度的运行队列指针 */ 2 struct sched_entity { 3 /* 权重,在数组prio_to_weight[]包含优先级转权重的数值 */ 4 struct load_weight load; /* for load-balancing */ 5 /* 实体在红黑树对应的结点信息 */ 6 struct rb_node run_node; 7 /* 实体所在的进程组 */ 8 struct list_head group_node; 9 /* 实体是否处于红黑树运行队列中 */ 10 unsigned int on_rq; 11 12 /* 开始运行时间 */ 13 u64 exec_start; 14 /* 总运行时间 */ 15 u64 sum_exec_runtime; 16 /* 虚拟运行时间,在时间中断或者任务状态发生改变时会更新 17 * 其会不停增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边被调度 18 * 每次时钟中断都会修改其值 19 * 具体见calc_delta_fair()函数 20 */ 21 u64 vruntime; 22 /* 进程在切换进CPU时的sum_exec_runtime值 */ 23 u64 prev_sum_exec_runtime; 24 25 /* 此调度实体中进程移到其他CPU组的数量 */ 26 u64 nr_migrations; 27 28 #ifdef CONFIG_SCHEDSTATS 29 /* 用于统计一些数据 */ 30 struct sched_statistics statistics; 31 #endif 32 33 #ifdef CONFIG_FAIR_GROUP_SCHED 34 /* 代表此进程组的深度,每个进程组都比其parent调度组深度大1 */ 35 int depth; 36 /* 父亲调度实体指针,如果是进程则指向其运行队列的调度实体,如果是进程组则指向其上一个进程组的调度实体 37 * 在 set_task_rq 函数中设置 38 */ 39 struct sched_entity *parent; 40 /* 实体所处红黑树运行队列 */ 41 struct cfs_rq *cfs_rq; 42 /* 实体的红黑树运行队列,如果为NULL表明其是一个进程,若非NULL表明其是调度组 */ 43 struct cfs_rq *my_q; 44 #endif 45 46 #ifdef CONFIG_SMP 47 /* Per-entity load-tracking */ 48 struct sched_avg avg; 49 #endif 50 };
操作系统(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。
七.参考资料