每个进程都具有自己的属性,用一个task_struct数据结构来表示,它包含了进程的详细信息,主要有进程标识符(PID)、进程所占的内存区域、相关文件描述符、安全信息、进程环境、信号处理、资源安排、同步处理状态几个方面。
数组task包含指向系统中所有task_struct结构的指针。创建进程时,Linux将从系统内存中分配一个task_struct结构,并将其加入task数组。操作系统初始化后,建立init进程,它建立一个task_struct数据结构INIT_TASK。当前运行进程的结构用current指针来指示。
进程切换包含三个层次:
1)用户数据的保存 包括正文段、数据段(DATA,BSS)、堆栈段(STACK)、共享内存段(SHARED MEMORY)。
2)寄存器数据的保护 包含PC、PSW(处理器状态字)、SP(栈指针)、PCBP(进程控制块指针)、FP(指向栈中一个函数的local变量的首地址)、P(指向栈中调用函数的实参位置)、ISP(中断栈指针),以及其他通用寄存器等。
3)系统级的保护 包括proc、u‘虚拟存储器空间管理表格、中断处理栈。
(PROC文件系统是Linux中的特殊文件系统,提供给用户一个可以了解内核内部工作过程的可读窗口,在运行时访问内核内部数据结构、改变内核设置的机制。)
状态为TASK_INTERRUPTBLE或TASK_UNINTERRUPTIBLE的睡眠进程得到它需要的资源被唤醒,通过schdule()进入TASK_RUNNING状态。状态TASK_UNINTERRUPTIBLE的睡眠进程,不能被信号或定时器中断唤醒,只有它申请的资源有效时才能被唤醒。
进程执行do_exit()后进入状态TASK_ZOMBILE,释放所申请的资源。
1.2 进程的创建1.2.1对象缓存的分配
在系统启动时的启动内核函数中,有进程管理的初始化函数,其中fork_init函数初始化线程数,分配进程结构的对象缓存。各个进程创建时进程结构对象从这里分配空间。
1.2.1系统调用sys_fork
当系统调用sys_fork创建一个进程的时候,它直接调用了实现函数do_fork。do_fork函数拷贝父进程的相关数据,如文件、信号量、内存等。完成进程初始化后,由父进程调用wake_up_process()函数将其唤醒,状态变为TASK_RUNNING,挂到就绪队列,返回子进程的pid。(创建完成的子进程会挂到就绪队列)
1.3 内核线程一个进程可以拥有多个线程。如果进程运行在SMP机器上,多个CPU执行各个线程,这样达到最大程度的并行。线程的上下文切换开销就比进程要小多了,线程共享了进程中除CPU以外的其他资源。
线程有内核线程、轻量级进程和用户线程三种,其中内核线程在内核调度,可并发使用多个处理器。用户线程在用户空间实现,它减少了上下文切换开销,它并行处理一个进程中的多个事务。
1)内核线程
内核线程是由内核创建和撤销的,用来执行一个指定的函数。内核线程共享内核的正文段内核全局数据,但各自具有自己的内核堆栈。它能够被单独调度,并且使用标准的内核同步机制,可以被单独分配到一个处理器上运行。内核线程实际上是一个与父进程共享地址空间的进程。
2)轻量级进程
轻量级进程是内核支持的用户线程。它在一个单独的进程中提供多线程控制。这些轻量级进程被单独调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上。轻量级进程不被独立调度,并且共享地址空间和进程中的其他资源,但是每个轻量级进程都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。
3)用户线程
用户线程是通过线程库实现的。它们是在没有内核的参与下进行创建、释放和管理的。线程库提供了同步和调度的方法。用户线程的上下文在没有内核干预的情况下保存和恢复。每个用户线程可以有自己的用户堆栈,一块用来保存用户级寄存器上下文,以及入如信号屏蔽等状态信息的内存区。
内核只调度用户线程下的进程,这些进程再通过线程库函数来调度它们的线程。
1.4 工作队列工作队列接口是用于调度内核工作任务的。每个工作队列使用一个专门线程,所有来自运行队列的工作任务在这个线程中运行,而线程是在进程的上下文中运行的。因此可在适当时间调度此线程来运行工作任务。
1.5 进程调度在linux中,每一个CPU维护一个自己的runqueue结构的就绪队列。
1.5.1 runqueue结构
runqueue结构是主要的CPU运行队列数据结构。运行队列用来按照优先级来管理进程,每个运行队列代表一定优先级的进程的链表。与工作队列的区别是:一个工作队列运用一个线程来运行工作队列中的各种工作任务,而内核线程实质也是一种进程,工作队列与运行队列是完全不相关的两个概念。
1.5.2 进程调度初始化
linux的进程有schedule函数执行。它只在内核态运行,任何进程从系统调用返回时会转入schedule()。大多数中断服务程序在中断响应完成后,也会转入schedule().
1)调度器的初始化
函数sched_init初始化调度器
2)进程调度器相关环境的建立
函数sched_fork为进程p建立调度器相关环境,进程p是由当前进程用函数fork()新建的进程。
在进程创建fork系统调用中会调用do_fork函数,在do_fork函数中会调用到copy_process函数,而copy_process函数调用到了sched_fork函数,这说明进程创建时,就会建立调度器相关环境。shed_fork函数给进程p分配时间片,打上时间戳。
1.5.3 函数schedule分析
进程的调度有直接启动调度和被动调度两种方式,在不同的方式下调度执行的步骤是不一样的:
1)直接启动调度
直接启动调度发生在当前进程因资源而需要进入被阻塞状态时。调度程序执行的步骤如下:
a 把当前进程放到适当的等待队列里;
b 把当前进程的state设为TASK_INTERRUPTIBEL或者TASK_UNINTERRUPTIBEL;
c 调用schedule(),准备让新的进程掌握CPU
d 检查当前进程所需的资源是否可用,如果是,则把当前进程从等待队列里删除。
2)被动调度
通过在当前进程的need_resched设为1来实现被动调度,每次调入一个用户态进程之前,这个变量的值都会被检查,来决定是否调用函数schedule()来实现调度
函数schedule的功能是选择一个合适的进程在CPU上执行,它的基本流程分为五个操作步骤:
1)清理当前运行中的进程
2)选择下一个投入运行的进程
3)设置新进程的运行环境
4)执行进程上下文切换
5)后期整理
1.6 Linux内核抢占内核抢占就是让调度程序能尽可能多地运行,从而减少了从一个事件发生到调度程序被执行的时间延迟。在当前进程具有被“安全”抢占条件,并且有一个等待处理的重新调度请求时,内核就调用调度程序来进行进程调度。
内核抢占要求内核中所有可能为一个以上进程共享的变量和数据结构都要通过互斥机制加以保护,或者说都要放在临界区中。在抢占式内核中,认为如果内核不是在一个中断处理程序中,并且不在spinlock保护的代码中,就任务可以“安全”地进行切换。
抢占式内核实现的原理是在释放spinlock时,或者当中断返回时,如果当前执行进程的need_resched被标记,则进行抢占式调度。