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的指针。根据这个原理,首先当前进程通过SP寄存器获取栈的位置,然后根据栈大小(一般为1-2页)获取thread_info的地址,最后通过thread_info获取当前进程的地址。
基于以上分析,进程的内核栈与thread_info存放在同一页内,thread_info与内核栈共享了页。从源代码中可以看出,在分配内核栈的同时也分配了thread_info。
linux不严格区分进程与线程,把进程与线程统称为轻量级进程,如果某个轻量级进程是线程组长,那么就是符合POSIX标准定义的进程概念;如果是线程组员,那么就是符合POSIX标准定义的线程概念。
根据POSIX标准,一个进程内的所有线程的ID就是该进程的ID,线程自己没有独立的线程ID;但是,linux不仅有进程ID,而且给每个线程也分配了线程ID。对于task_struct数据结构的成员来说,pid是线程ID,tgid是线程的进程ID(该进程也叫线程组长)。
经过上面的分析,就不难理解sys_getpid系统调用获取的是tgid;sys_gettid系统调用获取的是pid。总之,POSIX的进程ID就是tgid成员;POSIX的线程ID就是pid成员。
linux中有两个特殊的进程:进程0(swapper或者idle)和进程1(init)。系统在初始化时静态建立进程0,并分配了进程ID为0;进程1是进程0创建的第一个进程,所以进程1的进程ID为1。由于这两个进程在系统中的地位比较重要,因此通常称这两个进程为进程0、进程1;隐含的表达了进程0就是swapper、进程1就是init。
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成员记录了文件系统的命名空间。
进程之间存在父子、线程组、进程组等重要的组织关系。
task_struct的real_parent指向父进程、parent指向“养父进程“。父进程是实际创建子进程的进程;而“养父进程”是负责处理子进程消亡的进程,在大多数情况下,父进程就是“养父进程“。但是也有例外,例如,当父进程比子进程提前消亡时,父进程会帮子进程重新寻找一个“养父进程”,通常是进程1。children成员是父进程的子进程链表;sibling成员是子进程的兄弟进程链表。
task_struct的group_leader成员指向线程组长,thread_group成员是线程组长的线程链表。如果,当前线程组只有一个线程,那么该线程的group_leader成员指向自己,并且thread_group为空。
本文介绍了进程描述符数据结构,重点描述了进程资源、调度、组织结构等方面的信息。进程资源信息包括文件系统、地址空间、打开的文件等;调度信息包括进程优先级、当前状态、信号量等;组织结构信息包括进程父子关系、进程组等。
原创作品,如非商业性转载,请注明出处;如商业性转载出版,请与作者联系。