您的位置:首页 > 运维架构 > Linux

深入理解Linux内核个人小结3--进程

2012-08-31 17:11 666 查看
进程---程序执行的一个实例

一. 进程的静态特性

1.进程与线程:

Linux2.6内核版本支持多线程,现在大部分多线程应用程序都是使用pthread库编写的。 Linux2.6内核加入了对内核线程组的概念,每个进程描述符多加了一个tgid字段。

Linux通过轻量级进程来实现对多线程的支持,线程之间可以共享一些资源(地址空间、打开的文件等)。线程组:实现了多线程应用的一组轻量级进程。

进程与线程的区别:

进程:资源配给的单位,维护程序的静态资源(如地址空间、打开的文件集等)

线程:CPU调度的基本单位,维护调度相关的信息(如运行栈、调度控制信息等)

2.进程描述符:

进程的7种状态:可运行状态(TASK_RUNNING)、可中断的等待状态(TASK_INTERRUPTIBLE)、不可中断的等待状态(TASK_UNINTERRUPTIBLE)、暂停状态(TASK_STOPPED)、跟踪状态(TASK_TRACED)外加两种终止状态:僵死状态(EXIT_ZOMBIE)、僵死撤销状态(EXIT_DEAD)。

进程标识符PID:线程也具有PID以便其能够被CPU调度。 注意:在线程组中,每个线程的tgid字段都存有线程组领头线程的PID,而其本身的PID依然存储于PID字段中,getpid()系统调用返回的是当前进程的tgid而非PID字段。

因进程是动态实体,因而内核把进程存储在动态内存中。对于每个进程Linux总是把内核的进程堆栈与一个与线程描述符相关的小数据结构thread_info两者存放在连续两个页框中(共8KB大小)。 通过esp--->thread_info---->task便可以得到当前的进程描述符。

3.进程链表:都是双向循环链表,共有三种类型的链表

<1>. 将所有进程描述符都链接在一起的一个进程链表。

<2>. 可运行状态进程链表:每个优先级(0~139)一个进程链表。而在多处理器系统中情况更为复杂,每个CPU都有自己的运行队列(即每个cpu都有140个可运行进程链表)。

<3>. 等待进程链表:根据等待事件的不同把处于等待状态的进程(TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE)放入特定的等待队列。

4. 进程间的关系:

亲属关系:父、子、兄弟

非亲属关系:登陆会话、领头进程等

5. 进程的资源限制: 每个进程所能够获得的资源都不是无限的,其相应的都有一组相关的资源限制(指定了进程能够使用的系统资源数量),避免用户过度使用系统资源(CPU、硬盘空间等)。 每个进程的资源限制存放于 current->signal->rlim字段中。rlim是一个数组。

6. 进程散列表(pidhash):

顺序地扫描进程链表并检查描述符的PID字段太过耗时,为了加速查找PID引入了4个PID散列表。需要4个是因为进程描述符包含了4种不同类型的PID字段,而每种PID都需要自己的散列表。4种分别为:PID(进程PID)、TGID(线程组领头进程的PID)、PGRP(进程组领头进程的PID)、SESSION(回话领头进程的PID)。

二. 进程切换

1. 硬件上下文: 硬件上下文即进程在恢复执行前必须装入寄存器的一组数据。其是进程可执行上下文的一个子集。硬件上下文一部分放在TSS中,另一部分存放于内核态堆栈。进程切换只发生于与内核态。 用户态----->寄存器------>内核态堆栈、TSS 感觉寄存器像是用户态中的进程的上下文进入内核态的一个大门。

2. TSS:

<1>. 每个CPU一个TSS,存储在TR寄存器中且TR永远指向它。当进程切换时只需更新对应CPU的TSS中的esp0字段,让其指向新进程的内核态堆栈即可。

<2>. TSS只使用esp0和iomap字段,后者用来存储I/O许可权位图,esp0指向进程的内核态堆栈(上面保存有其他寄存器的值)。

3. 进程切换

进程切换时被切换出去的进程的硬件上下文保存在类型为thread_struct类型的thread字段,此字段是进程描述符的一部分。只要进程被切换出去内核就把其硬件上下文保存在这个结构之中。

进程切换只发生在指定的时间点:schedule()。进程切换由两步组成:

<1>. 切换全局目录并安装一个新的地址空间

<2>. 切换内核态堆栈及硬件上下文

三. 进程的创建与撤销

1.进程的创建

传统的Unix操作系统以同一方式对待所有进程:子进程复制进程所拥有的所有资源。这种方法因为子进程要拷贝父进程的整个地址空间从而导致进程的创建非常缓慢。

而现代Unix内核通过引入三种不同的机制解决了这个问题:

<1>. 写时复制技术,允许父子进程都相同的页

<2>. 轻量级进程允许父子进程共享很多数据结构(注意同步及临界区问题)

<3>. vfork系统调用创建的进程能够共享其父进程的内存地址空间,并阻塞其父进程的执行,直到子进程退出或执行一个新的程序为止。

Linux中轻量级进程通过clone()函数来创建,传统的fork()系统调用及前面提到的vfork()系统调用也都是通过clone()实现的只是其中指定了不同的参数。

clone()----->do_fork()----------->copy_process()

copy_process():创建进程描述符以及子进程执行所需要的所有其他数据结构。

do_fork():负责处理clone()的系统调用,并向下调用copy_pocess()。

2. 内核线程(也成后台守护线程)

内核线程只运行于内核态且只能使用大于PAGE_OFFSET的线性地址,而普通线程既可以运行于内核态也可以运行于用户态且可以使用全部的线性地址。

内核线程通过kernel_thread()函数进行创建,其本质也是通过调用do_fork()来完成的。

进程0:所有进程的祖先,也称idle进程或swapper进程。其是在Linux初始化阶段从无到有创建的一个内核线程。初始化部分内核空间然后创建进程编号为1的内核线程并与其共享所有的内核数据结构。然后初始化工作由1号进程接管,进程0开始执行cpu_idle()函数(本质为在开中断的情况下不断执行hlt汇编语言指令)。 每个CPU都有一个进程0,当开机打开电源后,计算机BIOS就启动某一个CPU同时禁用其他CPU。运行于CPU 0上的swapper进程初始化内核数据结构,然后激活其他CPU并用过copy_process()函数创建另外的swapper进程并把0传过去作为他们的新PID。

进程1:由0号进程创建的1号进程,其负责完成内核部分的初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟内存管理的内核线程。随后1号进程调用execve()运行可执行程序init(),并演变成用户态1号进程init。然后按照配置文件/etc/initab的要求,完成系统的启动工作并创建编号为1,2,3.。。的若干终端注册进程。每个getty进程设置其进程组标号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可以输入用户名和密码进入登录过程,若成功则有login程序再通过execv()执行shell,该shell进程接受getty进程的PID,并将其取代。再由shell间接地生成其他进程。

其顺序为:0号进程-->1号内核进程进程--->1号用户进程(init)-->getty进程---->login进程。

注意:

<1>..init()函数在内核态运行,是内核代码

<2>. init进程是内核启动并运行的第一个用户进程,运行在用户态下。

<3>. 1号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。

3. 进程的终止与删除

<1> .进程终止:

注意:kill()函数是向指定进程传递信号的,并不像Linux命令kill那样能够杀死进程。进程终止的方法一般是通过调用函数eixt()/exit_group(),前者杀死指定线程,而后者杀死属于当前线程组的所有线程。两者都是通过do_exit()函数来处理的,do_exit从内核数据结构中删除对终止进程的大部分引用。

<2>. 进程删除:进程终止以后,Linux内核并未立即丢弃包含在进程描述符中的数据。其处于僵死状态,父进程对僵死进程的处理有两种方式:

若父进程不需要来自子进程的信号,则直接调用do_exit(),内存回收将由进程调度程序来完成;若子进程已经给父进程发送了信号则父进程调用wait()类函数处理并回收进程描述符所占用的空间。

内核线程部分参考自: http://blog.csdn.net/yjzl1911/article/details/5613569
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: