通过Linux理解操作系统(三):进程管理(下)

  在前文我们大致了解了程序中如何使用系统调用实现我们想要的进程管理方式,在本文中我们将要看看linux系统内核又是如何实现进程的管理的。正如在概述中讲的,操作系统本身也只是一个计算机程序,只要是程序,就会有数据结构和算法,就同样会利用到内存空间甚至磁盘空间,在往下看之前,读者不妨先根据自己的知识思考一下可以用什么样的方式来实现,说不定就搞出了一个新的系统哦~

1、进程的表示方式

linux将一个个进程抽象为一个个任务,并定义了一个结构体task_struct用于表示一个任务,对于每一个进程,在其生命周期里都会有一个相应task_struct类型的进程描述符存在于内存中,保存了内核用于管理进程所需要的重要信息,一个task_struct包含了以下这些域:

  1. 调度参数:进程优先级,已使用的CPU时间,已休眠的时间,用于系统决定调度哪个进程执行;

  2. 内存镜像:指向进程textdatastack内存段或page table的指针;

  3. 信号:指定哪些信号将被处理,哪些信号将被忽略等;

  4. 寄存器:进程切换至内核模式时,用于保存当前正在运行的寄存器信息;

  5. 系统调用状态:保存当前进行系统调用的信息,包括参数,结果等;

  6. 文件描述符表:保存进程打开的文件的i-node数据;

  7. 统计信息:记录了进程使用的cpu时间,栈空间大小,分页帧数等;

  8. 内核栈空间:本进程专属的内核栈空间地址;

  9. 其他:当前进程状态,正在等待的事件,进程ID,父进程ID,用户ID等信息。

通过为每一个进程保存以上这些信息,系统内核才得以合理地进行进程的管理。比如当进行进程调度时,内核需要得到每个进程的优先级,来决定要分多少时间片;在进程接收到一个信号时,内核需要查看进程指定了什么方式进行处理,这些所有的信息都需要通过在进程描述符中查找。

由于系统中同时运行了多个进程,内存中也就保存了多个进程描述符,为了方便地管理和支持快速查找,内核维护了一个哈希表,使用PID作键值,采用开散列的方式解决冲突,同一个槽的元素使用双向链表连接,如下图所示:(只是示意图)

通过上面这种存储方式,当内核需要查找一个进程描述符时,只需要将进程的ID映射到哈希表的一个槽中,并在该槽的链表上进行顺序查找即可。接下来我们再看看一个进程在linux系统中是如何创建的,有了以上信息,这个过程就很容易理解了。

当一个fork系统调用执行时,调用fork的进程将切换至内核模式并创建一个task_struct类型的进程描述符(还有其他一些结构,这里就不提了),新创建的进程描述符中的大多数内容将根据父进程的进程描述符进行设置,然后系统分配一个新的PID,并根据这个PID,映射到哈希表里对应的槽,若该槽已被占,则新建一个元素添加到链表里,该元素保存了新的进程描述符的内存地址。接下来,系统再为子进程分配内存空间,并将父进程的内存空间中的内容复制过来,这个过程完成之后,子进程便可以开始运行了。

2、进程调度

在了解了进程的表示方式之后,我们再看看linux如何实现进程调度。

1)关于线程:linux系统的调度是基于线程的,一般将进程视为资源容器,而将线程视为一个执行单元(也就是一段连续,独立的执行过程),事实上前面对应的task_struct是对应了一个线程,一个单线程的进程表示为一个task结构,而一个多线程的进程有多个task结构,每一个线程有一个。(这里可能有点乱,因为linux里进程和线程的概念有些模糊,不像其他系统还区分了进程,轻量级进程和线程,不过我们编程的时候不需要考虑这么多,所以没什么影响)

2)优先级:linux将线程分成三类:实时FIFO线程、实时轮转线程、普通分时线程

其中实时跟普通分时的区别仅在于优先级不同,实时线程为0-99,普通分时线程为100-139(优先级总共140个,0-139),优先级数值越低表示优先级越高,而FIFO跟轮转的区别在于FIFO是非抢占的,即到达的线程任务必须完全完成之后,下一个线程任务才能进行,轮转则是每个线程分配了一个时间片,在该时间片内线程可以运行,当时间片耗完则切换下一个线程运行不管当前任务是否完成。

除了优先级之外,与进程调度有关的还有另外一个值叫NICE值,它的范围是-20~+19nice值的意思是表示其他进程的友好程度,友好是指把cpu时间让出来,所有一个进程的NICE值越大,它本身使用的cpu时间会越少,其默认的值为0,可以使用nice系统调用进行修改。优先级和NICE值共同决定了一个进程在cpu的运行时间。

(3)进程调度的数据结构:linux的调度器为每一个cpu维护了一个数据结构成为runqueue,示意图如下:

如上图所示,一个runqueue中有两个域activeexpired,它们是一个指针分别指向了一个长度为140的数组,而这每个数组里有存储了140个链表的头指针,每个链表实际上对应了一个优先级,链表里存了属于该优先级的进程。调度器进行调度的过程基本上是:先从active里优先级最高的链表中取出一个任务执行,当该进程的时间片耗光后,则把它移动expired里对应的优先级的链表中,然后再取出下一个进程执行,这样一级级往下,就保证了优先级高的进程先被执行,同时进程优先级越高,所分配的时间片也越长。而当active里的所有进程都已经执行过后,只要将activeexpired的这两个指针的指向交换,则原先expired里的现在进程现在又变成了active,之后再重复上面的步骤即可,这种方式就保证了低优先级的进程能够得到cpu时间。

OK,关于linux系统的进程管理的部分就到这里了,其实实现这一部分还是很复杂的,我的水平有限也没有办法讲得很清楚,而且因为对实际应用的影响也不是很大,所以也没什么兴趣进行再深入的研究了,我们只要知道它实现的机制和思路,证明它真的只是一个程序,而且设计的方式多种多样,没有绝对标准就达到目的了,下一篇将进入到linux的内存管理模块,敬请期待。

 

posted @ 2013-05-09 15:56  Benjamin-T  阅读(1987)  评论(3编辑  收藏  举报