linux支持多进程特性,可以最大化的使用cpu资源;用户可以在同一个cpu上运行多个用户程序。多进程的原理是:时钟中断触发进程调度程序,调度程序分时运行多个进程。这就要求每个进程能够保留现场信息(cpu现场、系统资源、调度信息等)。
linux使用进程描述符记录现场信息,然后基于进程描述符管理进程,包括进程的创建、调度、消亡等操作。
本系列文章将详细讲述进程管理相关的知识,内核版本为3.10,发行版为ubuntu12.04。
一、目的
在介绍进程管理之前,先介绍进程描述符的概念及现场信息,并阐述这些信息的具体含义。
二、进程描述符
进程不仅仅是运行着的程序,还包括拥有的系统资源、当前cpu现场、调度信息、进程间关系等重要信息,记录这些现场信息的结构就是进程描述符task_struct(可以在include/linux/sched.h中找到定义)。
每个进程都有一个进程描述符,记录以下重要信息:进程标识符、进程当前状态、栈地址空间、内存地址空间、文件系统、打开的文件、信号量等。
进程描述符在内存中的存放位置比较有特点。由于系统需要频繁的获取当前进程描述符的地址,为了提高效率,linux巧妙的实现该功能,使用current宏可以快速得到当前进程地址。在x86体系中,栈指针由专门的寄存器存放(SP寄存器),所以可以快速获取当前进程栈的位置;linux在栈的末端存放了一个特殊的数据结构thread_info,thread_info中存放了指向task_struct的指针。根据这个原理,当前进程首先通过栈寄存器获取栈的位置,然后根据栈大小(一般为1-2页)将栈指针移动到栈的末端获取thread_info的地址,最后通过thread_info获取当前进程的地址。
基于以上分析,进程的内核栈与thread_info存放在同一页内,thread_info与内核栈共享了页。从源代码中可以看出,在分配内核栈的同时也分配了thread_info。
三、进程标识符与进程组标识符
系统使用进程标识符pid标识每个进程,/proc/sys/kernel/pid_max文件记录了pid的最大值,用户可以修改这个文件改变系统标识符的最大值。
linux中有两个特殊的进程:进程0(swapper或者idle)和进程1(init)。系统在初始化时静态建立进程0,并分配了标识符0;进程1是进程0创建的第一个进程,所以进程1的标识符为1。由于这两个进程在系统中的地位比较重要,因此通常称这两个进程为进程0、进程1;隐含的表达了进程0就是swapper、进程1就是init。
linux不区分进程和线程,但是POSIX却明确区分了进程和线程,并且规定一个进程内部的多个线程要共享一个pid;但是,linux内核不论是进程还是线程,都是会分配一个唯一的pid(这时候,pid其实就是线程ID)。为了满足POSIX的线程规定,linux引入了线程组的概念,一个进程中的所有线程所共享的那个pid被称为线程组ID,也就是task_struct中的tgid成员。因此,在linux内核中,线程组ID(tgid,threadgroup id)就是传统意义的进程ID。
经过上面的分析,就不难理解sys_getpid系统调用获取的是tgid(线程组ID);sys_gettid系统调用获取的是pid(线程ID)。总之,POSIX的进程ID就是linux的tgid;POSIX的线程ID就是linux中的pid。
由于linux不严格区分进程和线程,所以后续统一使用“进程”表示进程或线程概念;使用“进程组”表示线程组概念。
四、进程名称
comm成员记录了当前进程可执行文件的名称,最大长度为16个字节。
五、进程状态
调度程序根据进程状态决定是否调度进程,linux是哟概念bitmap(位图)表示进程状态,一共有11种状态;这些状态可以分为三类:运行态、睡眠态、退出态。只有运行态的进程才能被调度程序调度;进程等待某个资源时处于睡眠态(可中断态、不可中断态);进程退出时处于退出态(僵尸态、死亡态)。其他的进程状态还包括停止态、跟踪态等,这些状态处于特定的使用场景中,就不介绍了。
运行态:进程在cpu上运行或者等待运行。
睡眠态:睡眠态分为可中断态和不可中断态。进程因为等待某个资源而处于睡眠状态。这两者的区别是不可中断态忽略发送过来的唤醒信号量,因为这个状态的进程获取了重要的系统资源,因此不能被轻易打断,该状态较少使用。
退出态:退出态分为僵尸态和死亡态。进程完成使命退出后处于僵尸态,此时进程的资源已经被释放,仅仅保留了task_struct结构(父进程可能使用);而死亡态不仅释放了所有资源,并且连task_struct结构也释放了。
此外,为了便于记忆,系统给每个状态分配了一个字母缩写“RSDTtZXxKWP”,对应关系如下图所示。
六、进程栈
linux系统为每个用户进程分配了两个栈:用户栈和内核栈。当一个进程在用户空间执行时,系统使用用户栈;当在内核空间执行时,系统使用内核栈。由于内核栈地址空间的限制,内核栈不会分配很大的空间。此外,内核进程只有内核栈,没有用户栈。
当进程从用户空间陷入到内核空间时,首先,操作系统在内核栈中记录用户栈的当前位置,然后将栈寄存器指向内核栈;内核空间的程序执行完毕后,操作系统根据内核栈中记录的用户栈位置,重新将栈寄存器指向用户栈。由于每次从内核空间中返回时,内核栈肯定已经使用完毕,所以从用户栈切换到内核栈时,只需要简单的将栈寄存器指向内核栈顶即可,不需要做什么特殊处理。
七、进程运行时间
因为进程是分时运行的,所以有必要记录每个进程实际占用的cpu时间;进程描述符的utime、stime成员记录微秒、秒级的cpu时间。
start_time成员记录了创建进程的时间;real_start_time成员记录了启动进程的时间。
八、文件资源
进程描述符中还记录了和文件相关的信息:文件系统、打开的文件、命名空间等。
struct fs_struct *fs成员记录了根目录和当前目录信息;struct files_struct *files成员记录了进程当前打开的文件;struct nsproxy *nsproxy成员记录了文件系统的命名空间。
九、总结
本文介绍了进程描述符数据结构,重点描述了进程资源、调度、组织结构等方面的信息。进程资源信息包括文件系统、地址空间、打开的文件等;调度信息包括进程优先级、当前状态、信号量等;组织结构信息包括进程父子关系、进程组等。
版权声明:
原创作品,如非商业性转载,请注明出处;如商业性转载出版,请与作者联系。