天天看点

linux内核-进程

进程是多道程序设计系统的操作系统的基本概念,通常把进程定义为一个程序的执行实例。

进程

程序在一个数据实例上的一次执行过程,资源分配的基本单位

轻量级进程

在计算机操作系统中,轻量级进程(LWP)是一种实现多任务的方法。与普通进程相比,LWP与其他进程共享所有(或大部分)它的逻辑地址空间和系统资源;与线程相比,LWP有它自己的进程标识符,优先级,状态,以及栈和局部存储区,并和其他进程有着父子关系;这是和类Unix操作系统的系统调用vfork()生成的进程一样的。另外,线程既可由应用程序管理,又可由内核管理,而LWP只能由内核管理并像普通进程一样被调度。Linux内核是支持LWP的典型例子。两个轻量级进程基本上可以共享一些资源,只要一个进程修改,另外一个进程就查看修改,达到同步。

线程:进程的进一步划分,调度的基本单位,一个进程的多个线程共享进程的资源。

进程描述符

为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如内核必须知道进程的优先级,在cpu上运行还是被阻塞等等,这就是进程描述符的作用

进程描述符中包含:进程的基本信息,指向内存区描述符的指针 当前目录 指向文件描述符的指针 所接收的信号等等

进程的状态:

可运行状态:进程要么在cpu上执行,要么准备执行

可中断状态:进程被挂起,知道某个条件为真,产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件

不可中断状态:把信号传递给一个睡眠进程也不能改变它的状态,这种状态很少用到,但在某些情况下有用。例如,当进程打开一个设备文件,其相应的设备探测程序开始探测相应的设备时会用到这种状态。探测完成前,设备驱动程序不能被中断。

暂停状态:进程的执行被暂停

跟踪状态:进程的执行由debugger程序暂停。当一个进程被另一个进程监控时,任何信号都可以把这个进程至于跟踪状态。

僵死状态:进程被终止,但父进程还没有调用wait4或waitpid返回死亡进程的信息。

僵死撤销状态:由父进程发出wait4或waitpid调用,因而进程由系统删除。为了防止其他执行进程也执行wait()调用,而把进程状态改为僵死撤销状态

标识一个进程:一般来说,能被独立调度的每个执行上下文都必须拥有自己的进程描述符;因此,即使共享内核大部分数据结构的轻量级进程,也有他们自己的task_struct结构。类unix系统允许用户使用一个叫做进程标识符pid的数字来标识进程,pid的值存放在进程描述符的PID字段中。pid被顺序编号,缺省情况下pid最大值为32767,pid号循环使用,内核通过管理一个pid使用位图表示当前分配的pid号和闲置的pid号。

另一方面,unix程序员希望同一个组中的线程有共同的pid。例如把指定的pid的信号发送给组内的所有线程。事实上,posix1003.1.c标准规定一个多线程应用程序中的所有线程都必须具有相同的pid。linux引入线程组的概念,一个线程组中的所有线程使用改线程组的领头线程的pid,也就是改组的第一个轻量级进程的pid,它被存入到进程描述符的tgid字段。

进程描述符的处理:内核必须能够同时处理很多进程,并吧进程描述符放在动态的内存中,而不是放在永久的内核内存区。对每个进程来说,linux都把两个不同的数据结构紧凑的存放在一个单独为进程分配的存储区域中,一个是内核态的进程堆栈,另个一紧挨着的是进程描述符的小数据结构胶线程描述符thread-info,这块存储域的大小通常为8k;考虑效率问题,内核让这8k空间占连续的两个页框并让第一个页框的起始地址是2的13次方的倍数。c语言中使用联合结构体表示一个进程的线程描述符描述符和内核栈

union thread_info

{

struct thread_info thread_info;

unsigned long stack[2048];

}

thread_info大小为52个字节,因此内核堆栈能扩展到8140个字节。

thread_info从低地址开始存放,stack从高地址开始存放。esp为栈顶指针。

标识当前进程:内核很容易从esp寄存器的值获得当前在cpu上正在运行进程的thread-info结构的地址。事实上,因为thread-info结构的大小为8k,则内核屏蔽掉esp的低13位就可以获得thread-info的基地址。

进程最常用的是进程描述符的指针,current_thread_info->task,

task字段在threadinfo中的偏移量为0,所以执行完这后,就可获得包含在cpu上运行进程的描述符的指针。

双向链表:对于每个链表,必须实现一组原语操作:初始化链表,插入和删除一个元素,扫描链表等等。linux内核定义了list_head结构,字段next和prev分别表示通用双向链表的向前或向后的指针元素。

进程链表:进程链表的投是init_task进程,踏实所谓的0进程或swapper进程的进程描述符。

TASK_RUNNING状态的进程链表:当内核寻找一个新进程在cpu上运行时,必须只考虑可运行进程。早起的linux版本把所有的可运行进程放在一个可运行队列的链表中,由于维持链表中的进程按优先级排序开销过大,因此,早期的调度程序不得不为选择最佳的调度消耗过多的时间。

linux队列实现的运行队列有所不同。其目的是让调度程序能在固定时间内选出最佳的可运行进程。后面会在进程调度中回详细讲到这种新的运行队列。

提高程序运行速度的一个方法是根据优先级建立多个可运行队列,在多处理机系统中,每个cpu都有它自己的运行队列,即他自己的进程链表集。调度程序的速度的确提高了,但是运行队列的链表却因此拆分了很多个。

进程之间的关系:程序创建的进程具有父子关系。如果一个进程创建多个子进程时,则子进程之间具有兄弟关系。树形关系采用孩子兄弟表示法。

进程之间还有其他关系:一个进程可能是一个进程组或登录会话的领头进程,也可能是一个线程组的领头进程。还可能跟踪其他进程的执行。

pidhash表以及链表

在几种情况下,内核必须能从进程pid导出对应的进程的进程描述符的指针。例如kill系统调用时提供服务是会发生以下情况,当进程p1希望向进程p2发送一个信号是,p1调用kill系统调用,其参数是p2的pid,内核从这个pid导出其对应的进程描述符,然后从p2的进程描述符中取出记录挂起信号的数据结构的指针。

顺序扫描进程链表并检查进程描述符的pid字段是可行,但非常低效的。为了加速查早,引入了4个散列表,进程描述符包含了表示不同类型pid的字段,而且每种类型的pid需要它自己的散列表。

4个散列表:进程的pid;线程组领头进程pid;进程组领头进程pid;会话领头进程pid;

内核初始化期间动态为4个散列表分配空间,并把他们的地址存入pid_hash数组,一个散列表的长度依赖于可用的RAM容量。例如:一个系统拥有512M的RAM,那么每个散列表就被存在4个页框中,可以拥有2048个表项。

冲突解决:链地址法。

如何组织进程:运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起。当要把其他状态的进程分组时,不同的状态要求不同的处理,linux选择下列方式之一:

没有为TASK_STOPPED、EXIT_ZOMBIE或EXIT_DEAD状态的进程建立专门的链表,由于对出于暂停、僵死,死亡状态的进程访问比较简单,所以不必为这三种进程分组。

等待队列:进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待事件经过固定的间隔。等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒他们。因为等待队列是由中断处理函数和内核修改的,因此必须对其双向链表进行保护以免对齐进行同时访问,因为同时访问会导致不可预测的结果。同步,是通过等待队列的投中的lock自旋锁达到的。等待队列中的每个元素代表一个睡眠进程,该进程等待某一事件的发生;它的描述符地址存放在task字段中,task_list字段中保护的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中。然而要唤醒等待队列是有时并不方便,如果有两个或多个队列等待统一资源,仅唤醒等待队列中的一个进程才有意义。这个进程占有资源,而其他进程睡眠。

因此有两种进程:互斥进程由内核有选择的唤醒,而非互斥进程总是由内核在事件发生时唤醒。等待访问临界资源的进程就是互斥进程的例子,等待相关事件的进程是非互斥的。

进程资源限制:每个及承诺都有一组相关的资源限制,限制指定了进程所能使用的系统资源数量。这些限制避免用户过分使用系统资源。包括:

进程地址空间的最大数;内存信息转储文件大小;进程使用cpu的最长时间;堆大小的最大值;文件大小的最大值;文件锁的最大值;非交换内存的最大值;posix消息队列的最大字节数;打开文件描述符的最大值;用户能拥有的进程最大数;进程所拥有的页框最大数;进程挂起信号的最大数;栈大小的最大值。

进程切换:为了控制进程的执行,内核必须有能力挂起正在cpu上执行的进程,并恢复以前挂起的某个进程执行,这种行为叫进程的上下文切换。

硬件上下文:尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享cpu寄存器。因此,在恢复一个进程执行前,内核必须确保每个寄存器装入了挂起时进程的值。进程恢复执行前必须装入寄存器的一组数据称为上下文。在linux中,进程硬件上下文的一部分放在TSS段,而剩余部分存放在内核态的堆栈。早期的linux版本利用硬件完成上下文切换,基于以下原因,linux2.6使用软件执行上下文切换:

1、通过一组mov指令逐步完成切换,这样能更好地控制所装入数据的合法性。尤其,这使检查ds,es段寄存器的值成为可能。

2、新旧方法时间上大致相同。然而,尽管当前的切换代码还有改进的余地,却不能对硬件上下文进行优化。进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器的内容全部保存在内核态堆栈中。

TSS任务状态段:用来存放硬件上下文,尽管并不用硬件上下文切换,但还是为每个cpu创建了一个tss段:

1、当x86的一个cpu从用户态切换到内核态时,它就从TSS段获取内核态堆栈的地址

2、当用户进程试图通过in或out指令访问一个I/O端口时,cpu需要访问存放在TSS段的I/O许可权的位图。

TSS反应了cpu上的当前进程的特权级,但不必为没有运行在cpu上的进程保留TSS。

每个进程切换出去,内核就把其硬件上下文保存在进程描述符的thread_struct的thread字段,这个数据结构包含了大部分cpu寄存器,但不包括eax,ebx等等这些通用的寄存器,他们的值保留在内核堆栈中。

创建进程:传统的unix进程以统一的方式对待所有子进程:子进程复制父进程的所有资源,这种方法使进程的创建非常慢且非常低效,因为子进程要拷贝父进程的整个地址空间,实际上子进程几乎不必读或修改父进程的资源,很多情况下,子进程立即调用execve(),并清除父进程仔细拷贝过来的地址空间。

现代unix引入三种不同的机制来解决这个问题:

1、写时复制技术允许父子进程读相同的物理页。只要两者中有一个试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个物理页分配给正在写的进程。

2、轻量级进程允许父子进程共享每个进程在内核的很多数据结构。

3、vfork系统调用创建的进程能共享父进程的内存地址空间。为了防止父进程重写子进程时需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新程序为止。

内核线程与普通线程相比:

1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态

2、因为内核线程只运行在内核态,因为他们只是用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程可以使用4G的线性地址空间。

继续阅读