从内核的观点来看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体。
尽管父子进程共享含有程序代码的页,但是他们各自有独立的数据拷贝(堆和栈),因此子进程对一个内存单元的修改对父进程是不可见的。
多线程应用程序多个执行流的创建,处理,调度整个都是在用户态进行的(通常使用POSIX兼容的pthread库)。
两个轻量级进程基本上可以共享一些资源,诸如地址空间,打开的文件等等,只要其中一个修改共享资源,另一个就可以立即查看这种修改。
进程描述符中state字段描述了进程当前所处的状态:
1.TASK_RUNNING 2.TASK_INTERRUPTIBLE 3.TASK_UNINTERRUPTIBLE 4.TASK_STOPPED 5.TASK_TRACED
还有两个进程状态是既可以存放在进程描述符的state字段中,也可以存放在exte_state字段中
1.EXIT_ZOMBIE 2.EXIT_DEAD(由于父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除)
内核也使用set_task_state()和set_current_state()宏,他们分别设置指定进程的状态和当前执行进程的状态。
一般来说,能被独立调度的每个执行上下文都必须拥有它自己的进程描述符(task_struct)。
内核对进程的大部分引用是通过进程描述符指针进行的。
PID被顺序编号,新创建进程的PID通常是前一个进程的PID加1.
一个线程组中的所有线程使用和该线程组的领头线程(thread group leader)相同的PID,也就是该组中第一个轻量级进程的PID,他被存入进程描述符的tgid字段中。
内存必须能够同时处理很多进程,并把进程描述符存放在动态内存中,并不是放在永久分配给内核的内存区。????
linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是与进程描述符相关的小数据结构thread_info,叫做线程描述符。另一个是内核态的进程堆栈。这个存储区域的大小通常为8192个字节。
当使用一个页框存放内核态堆栈和thread_info结构时,内核要采用一些额外的栈防止中断和异常的深度嵌套而引起的溢出。
从用户态刚切换到内核态以后,进程的内核栈总是空的。
为了获得当前在cpu上运行进程的描述符指针,内核要调用current宏,该宏本质上等价于cuuent_thread_info()->task;
对每个链表,必须实现一组原语操作,初始化链表,插入和删除一个元素,扫描链表等。
进程链表把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks字段。
进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。
for_each_process()宏的功能是扫描整个进程链表。
enqueue_task()函数把进程描述符插入某个运行队列的列表。
进程0和进程1是由内核创建的。
顺序扫描进程链表并检查进程描述符的pid字段是可行但相当低效的。为了加速查找引入了几个散列表。
内核初始期间(pidhash_init)动态地分为几个散列表分配空间,并把它们的地址存入pid_hash数组。
两个不同的PID散列到相同的表索引称为冲突。linux利用链表来处理冲突的PID。
运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起。
等待队列在内核中有很多用途,尤其用在中断处理,进程同步及定时。
等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒他们。
等待队列由双向链表实现。每个等待队列都有一个等待队列头,等待队列头是一个类型为wait_queue_head_t的数据结构,同步是通过等待队列头中的lock自旋锁达到的。
等待队列链表中的每个元素代表一个睡眠进程,该进程等待某一事件的发生。
有两种睡眠进程,互斥进程(等待队列元素的flags字段为1)由内核有选择的唤醒,而非互斥进程(flags值为0)总是由内核在事件发生时唤醒。
等待队列元素的func字段用来表示等待队列中睡眠进程应该用什么方式唤醒。
用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新等待队列的头,它静态地声明一个叫name的等待队列的头变量并对该变量的lock和task_list字段进行初始化。函数init_waitqueue_head()可以用来初始化动态分配的等待队列的头变量。
也可以选择DEFINE_WAIT宏声明一个wait_queue_t类型的新变量,并用CPU上运行的当前进程的描述符和唤醒函数autoremove_wake_function()地址初始化这个新变量。
内核开发者可以通过init_waitqueue_func_entry()函数来自定义唤醒函数,该函数负责初始化等待队列的元素。
一旦定义了一个元素,必须把它插入等待队列。add_wait_queue()函数把一个非互斥进程插入等待链表的第一个位置。
在linux2.6中引入prepare_to_wait(),prepare_to_wait_exclusive()和finish_wait()函数提供了另外一种途径来使当前进程在一个等待队列中睡眠。
sleep_on()函数在一下条件下不能使用,那就是必须测试条件并且当条件还没有得到验证时又紧接着让进程去睡眠。
每个进程都有一组相关的资源限制,限制制定了进程能使用的系统资源数量。对当前进程的资源限制存放在current->signal->rlim字段。
尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文,硬件上下文是进程可执行上下文的一个子集。
进程切换只发生在内核态,在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。
每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。
从本质上讲,每个进程切换两步组成:
1.切换页全局目录以安装一个新的地址空间
2. 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息。
switch_to宏是内核中与硬件关系最密切的例程之一。
在任何进程切换中,涉及到三个进程而不是两个进程。
1. 写时复制技术允许父子进程读相同的物理页。只要两者中有一个试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。
2.轻量级进程允许父子进程共享每进程在内核的很多数据结构如页表(也就是整个用户态地址空间),打开文件表及信号处理
3.vfork()系统调用创建的进程能共享其父进程的内存地址空间
在linux中,轻量级进程是由名为clone()的函数创建的。
实际上clone()是在C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏的clone()系统调用。
传统的fork()调用在linux中是用clone()实现的,其中的clone()的flags参数指定为SIGCHLD信号以及所有清零的clone标志,而它的child_stack参数是父进程当前的堆栈指针。
vfork()系统调用在linux中也是用clone()实现的,其中clone()的参数flags指定为SIGCHLD信号和CLONE_VM以及CLONE_VFORK标志,clone()的参数child_stack等于父进程当前的栈指针。
do_fork()函数负责处理clone(),fork(),vfork()系统调用。利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。
在linux中,内核线程在以下方面不同于普通进程:
1 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态
2 因为内核线程只运行在内核态,他们呢只能使用大于PAGE_OFFSET的线性地址空间。另外不管用户态还是内核态,普通进程可以用4GB的线性地址空间。
kernel_thread()函数创建一个新的内核线程。
所有进程的祖先叫做进程0,idle进程,或因为历史原因叫做swapper进程,它是linux的初始化阶段从无到有创建的一个内核线程。这个祖先进程使用静态分配的数据结构(所有其他进程的数据结构都是动态分配的).
start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另外一个进程1的内核线程(一般叫做init进程)。新创建内核线程的PID为1,并与进程0共享每进程所有的内核数据结构。
创建init进程后,进程0执行cpu_idle()函数。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0.
init内核线程变为一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
进程终止的一般方式是调用exit()函数,该函数释放C函数库所分配的资源,执行编程所注册的那个函数,并结束从系统回收进程的那个系统调用。
所有进程的终止都是由do_exit()函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分引用。
Unix允许进程查询内核以获得其父进程的PID,或者其任何子进程的执行状态。
releas_task(0函数从僵死进程的描述符中分离出最后的数据结构,对僵死进程的处理有两种可能的方式
1如果父进程不需要接受来自子进程的信号,就调用do_exit();内存的回收将由进程调度程序来完成
2如果已经给父进程发送了一个信号,就调用wait4()和waitpid()系统调用,函数还将回收进程描述符所占用的内存空间
linux调度基于分时技术,多个进程以”时间多路复用“方式运行。因为CPU的时间被分成”片“,给每个可运行进程分配一片。
交互式进程相对有较高的优先级。
在linux中,进程的优先级是动态的。
在linux中,调度算法可以明确的确认所有实时程序的身份,但没有办法区分交互式程序和批处理程序。
注意被抢占的进程没有被挂起,因为它还处于TASK_RUNNING状态,只不过不再使用CPU。
调度程序总能成功地找到要执行的进程。事实上,总是至少有一个可运行进程,即swapper进程,它的PID等于0,而且它只有在CPU不能执行其他进程时才执行。
调度算法根据进程是普通进程还是实时进程有很大的不同。
每个普通进程都有他自己的静态优先级,调度程序使用静态优先级来估计系统中这个进程与其他普通进程之间的调度的程度。内核从100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。
新进程总是继承其父进程的静态优先级。
静态优先级本质上决定了进程的基本时间片。
普通进程除了静态优先级,还有动态优先级,其值的范围ieshi100-139.动态优先级是调度程序在选择新进程来运行的时候使用的数。
平均睡眠时间是进程在睡眠状态所消耗的平均纳秒数。进程在运行的过程中平均睡眠时间递减,最后,平均睡眠时间永远不会大于1s。
平均睡眠时间也被调度程序用来确定一个给定进程是交互式进程还是批处理进程。
一个具有缺省静态优先级(120)的进程,一旦其平均睡眠时间超过700ms,就成为交互式进程。
活动进程:还没有用完他们的时间片,因此允许他们运行
过期进程:可运行进程已经用完了他们的时间片,因此被禁止运行,直到所有活动进程都过期。
每个实时进程都与一个实时优先级相关,实时优先级是一个范围从1-99的值,调度程序总是让优先级高的进程运行,与普通进程相反,实时进程总是被当成活动进程。
如果几个可运行的实时进程具有相同的最高优先级,那么调度程序选择第一个出现在与本地CPU的运行队列相应链表中的进程。
当系统调用nice()和setpriority()用于基于时间片轮转的实时进程时,不改变实时进程的优先级而会改变其基本时间片的长度。实际上,基于时间片轮转的实时进程的的基本时间片的长度与实时进程的优先级无关,而依赖于进程的静态优先级。
数据结构rq是调度程序最重要的数据结构,系统中的CPU都有它自己的运行队列。宏this_rq()产生本地CPU运行队列的地址,宏cpu_rq(n)产生索引为n的CPU运行队列的地址。
系统中的每个可运行进程属于且只属于一个运行队列。只要可运行进程保持在同一个运行队列中,那就只可能在拥有该运行队列的CPU上执行。
一般来说,内核总是尽量推迟给用户态进程分配动态内存。
当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。
每个进程所看到的线性地址集合是不同的,一个进程所使用的地址与另外一个进程所使用的地址并没有什么关系。
确定一个进程当前所拥有的线性区(即进程的地址空间)是内核的基本任务。
内核通过提供页框来处理缺页异常,并让进程继续执行。
与进程地址空间有关的全部信息都包含一个叫做内存描述符(mm_struct)的数据结构中。
所有的内存描述符存放在一个双向链表中,每个描述符在mmlist字段存放链表相邻元素的地址。链表的第一个元素是init_mm的mmlist字段,init_mm是初始化阶段进程0使用的内存描述符。
mm_user字段存放共享mm_struct数据结构的轻量级进程的个数。