进程
进程是任何多通道程序设计的操作系统中的基本概念,进程通常被定义为程序执行时的一个实例,在 Liunx 的源代码中,进程通常被称为 “任务”。
进程描述符
进程描述符的作用是为了管理进程,内核必须对每个进程所做的事情进行清除的描述,例如,内核必须知道进程的优先级、进程状态、为它分配什么样的地址空间、允许访问那些文件等等;
进程描述符是 task_struct 类型结构,它的域包含了与一个进程相关的所有信息。
进程状态
进程描述符中的状态域描述了进程当前所处的状态:
可运行状态(TASK_RUNNING):进程要么在 CPU 上执行,要么准备执行;
可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起(睡眠),直到一些条件变为 真(产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号,都能唤醒进程,回到 TASK_RUNNING;
不可终端的等待状态(TASK_UNINTERRPTIBLE):与前一个状态类似, 但有一个例外,把信号传递到睡眠的进程不能改变它的状态,用在进程必须等待,不能被中断,知道给定的事件发生;
暂停状态(TASK_STOPPED):进程的执行被暂停,收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU 信号,进人暂停状态,当一个进程被另一个进程监控时,任何信号都可以把这个进程置于 TASK_STOPPED 状态;
僵死状态(TASK_ZOMBIE):进程的执行被终止,但是父进程还没有发布 wait() 类系统调用以返回有关死进程的信息,发布 wait() 类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它;
标识一个进程
Linux 进程能共享内核大部分数据结构(通过轻量级进程);
Linux 能处理多达 NR_TASKS 个进程,内核在自己的地址空间保存了一个全局静态数组 task,大小为 NR_TASKS,数组中的元素就是进程描述符指针,空指针表示数组项中没有进程描述符。
进程描述符的存放:
task 数组仅仅包含进程描述符的指针,而不是描述符本身,因为进程是动态实体,因此,进程描述符被存放在动态内存中,而不是存放永久的分配给内核的内存区。
内核态的进程访问包含在内核数据段中的栈,内存区中存放进程描述符和内核态的进程栈:
esp 寄存器是 CPU 栈指针,用来存放栈顶的地址,从用户态切换到内核态以后,进程的内核态堆栈总是空的,因此,esp 寄存器直接指向这个内存区的顶端。
C 语言中用联合结构表示这个混合结构:
union task_union {
struct task_struct task;
unsigned long stack[2048];
};
current 宏
进程描述符与内核堆栈之间的配对:内核很容易从 esp 寄存器的值获得当前在 CPU 上正在运行的进程描述符指针。
假设内存区是 8KB(2^13)长,内核必须让 esp 至少有 13 位有效位,以获得进程描述符的基础址,由 current 宏完成:
movl $0xffffe000, %ecx
andl %esp, %ecx
movl %ecx, p
执行三条指令后,局部变量 p 包含了在 CPU 上运行的进程描述符指针;
进程链表
为了对给定类型的进程进行有效的搜索,内核建立了几个进程链表,每个进程链表由指向进程描述符的指针组成;
一个双向循环链表把所有现有的进程联系起来,称为进程链表(process list),每个进程的 prev_task 和 next_task 域用来实现链表,链表的头是 init_task 描述符,由 task 数组的第一个元素指向,是进程的祖先,叫做进程 0 或 swapper,init_task 的 rev_task 域指向链表中最后插入的进程描述符。
SET_LINKS、REMOVE_LINKS 宏用来分别在进程链表中插入和删除一个进程描述符,for_each_task 宏扫描整个进程链表:
#define for_each_task(p) \
for (p = &init_task; (p = p->next_task) != &init_task ;)
TASK_RUNNING 状态的进程有独立的双向循环链表:运行队列(runqueue)。
pidhash 表及连接表:从进程的 PID 导出对应的进程描述符指针。
task 空闲表项的链表:进程创建或撤销都要更新。
进程之间的亲属关系
进程 0 和进程 1 由内核创建,进程 1(init)是所有进程的祖先,一个进程 P 的描述符包含下列域:
p_opptr —— 祖先(original parent):p_opptr 指向创建了进程 P 的进程描述符,如果父进程不存在,则指向 1,当一个 shell 用户启动一个后台进程并从 shell 退出时,后台进程变成 init 的子进程;
p_pptr —— 父进程(parent):p_pptr 指向 P 的当前父进程,值通常与 p_opptr 一致,但偶尔不同,当另一个进程发布 parace() 系统调用请求监控 P 时;
p_cptr —— 子进程(child):p_cptr 指向 P 年龄最小的子进程的描述符,即指向刚刚由 P 创建的进程的进程描述符
p_ysptr —— 弟进程(younger sibling):p_ysptr 指向在 P 之后由 P 的父进程马上创建的进程的进程描述符
p_osptr —— 兄进程(younger sibling):p_ysptr 指向在 P 之前由 P 的父进程马上创建的进程的进程描述符
等待队列
把 TASK_INTERRUPTIBLE 或 TASK_UINTERRUPTIBLE 状态的进程分成很多类,每一类对应一个特定的事件,进程状态提供的信息满足不了快递检索进程,引入了另外的进程链表 —— 等待队列(wait queue)
等待队列对中断处理、进程同步及定时用处很大,进程必须经常等待某些事件的发生,等待队列实现在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权,因此等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们;
等待队列中每个元素都是 wait_queue 类型:
struct wait_queue {
struct task_struct * task;
struct wait_queue * next;
}
每一个等待队列由一个等待队列指针来标识,等待队列指针或者指向链表中第一个元素地址,wait_queue 数据结构的 next 域指向链表中的下一个元素;
进程的使用限制
进程与一组使用限制(usage limit)相关联,使用限制指定了进程能使用的系统资源数量:
RLIMIT_CPU:进程使用 CPU 的最长时间,如果进程超过了这个限制,内核就向它发一个 SIGXCPU 信号,如果进程还不终止,再发一个 SIGKILL 信号;
RLIMIT_FSIZE:允许文件大小的最大值,如果进程试图把文件的大小托充到大于这个值,内核就给这个进程发 SIGXFSZ 信号;
RLIMIT_DATA、RLIMIT_STACK、RLIMIT_CORE、RLIMIT_RSS、RLIMIT_NPROC、RLIMIT_NOFILE、RLIMIT_MEMLOCK、RLIMIT_MEMLOCK、RLIMIT_AS 等等限制;
使用限制被存放在进程描述符的 rlim 域:
struct limit {
long rlim_cur;
long rlim_max;
}
rlim_cur 域是资源当前使用限制,例如:current->rlim[RLIMIT_CPU].rlim_cur 表示在 CPU 上正在允许进程所花时间的当前限制;
rlim_max 域是资源限制所允许的最大值,利用 getrlimit() 和 setrlimit() 系统调用,可以把 rlim_cur 增加到 rlim_max;
进程切换、创建进程、撤销进程
单独博客