进程管理的基础

一.进程管理的基本概念

1.进程四要素:
     有一段可执行的代码,代码不一定时进程专有,可以与其他进程共享;
     有起码的私有财产,就是系统专有的系统堆栈空间;
     有户口,就是内核中的task_struct数据结构,称为"进程控制块",这个结构是进程的"财产登记卡",记录着进程所占有的各项资源;
     有独立的存储空间,就是拥有专有的用户空间,进一步意味着除了有前述的系统堆栈外还有专门的用户堆栈.
    这四个必要条件缺少一个就不称其为"进程".如果具备前三条而少第四条,那就是"线程".特别的,如果完全没有用户空间,就称为"内核线程(kernel thread),如果共享用户空间就称为"用户线程".通常,为不引起混淆,二者统称为"线程".kswapd就是个内核线程.i386系统机构中一种新的段,叫做"任务状态栈"TSS.他和代码段和数据段等类似,也是一个"段",实际上只有104字节的数据结构.或口控制块.用以记录一个任务的关键的状态信息.
2.进程描述符:都是task_struct类型的结构,他的字段包含一个与进程相关的所有信息:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有一些包含指向其他数据结构的指针等。
    涉及的数据结构:thread_info 进程的基本信息 mm_struct 指向内存区描述符的指针 tty_struct 与进程相关的tty 
fs_struct 当前目录 files_struct 指向文件描述符的指针 signal_struct 所接收的信息
3.进程的状态:可运行状态(TASK_RUNNING) 可中断等待状态(TASK_INTERRUPTIABLE) 不可中断的等待状态 (TASK_UNINTERRUPTIABLE)
暂停状态(TASK_STOPPED) 跟踪状态(TASK_TRACE) 僵死状态(TASK_ZOMBLE)僵死撤销状态(TASK_DEAD)
4.进程描述符的处理:
    1)linux把二个不同的数据结构紧凑的存放在一个单独为进程分配的存储区域:thread_info(线程描述符)和内核态的堆栈。
为了效率而言,分配二个连续的页框(8Kb)并让第一个页框从起始地址是2的13次方的倍数。
    2)esp寄存器存放cpu的栈指针,用来存放栈顶单元的地址。当用户态切换到内核态时,进程的内核态总是空的,所以esp指向的是这个栈的顶端,当有数据写入时,esp递减。
5.标识当前进程:内核态堆栈和thread_info的处理的好处在于可以很容易的从cpu的esp寄存器计算到正在运行进程的thread_info机构的基地址。如何采用的二个页框(8kb),则内核屏蔽掉esp的低13位就可得到,如果采用的一个页框(4kb),则只用屏蔽掉esp的低12位。
这个过程一般通过汇编实现。
    获得进程描述符的地址:进程最常用的是进程描述符的地址而不是thread_info的地址。为了获得当前进程的描述符指针,内核调用宏current,改宏的本质是current_thread_info()->task,它产生汇编指令。在对处理器上,把current定义为一个数组,每一个元素对应一个cpu,这样current就可以标识一个正在运行进程的描述符。
6.如何组织进程?
    1)等待队列:等待队列在内核中非常重要,尤其在中断处理,进程同步和定时。他表示一组睡眠的进程,当某一个条件满足时,由内核唤醒他们。等待队列是通过双链表实现的,其元素包括指向进程描述符的指针。
    2)等待队列头:它是一种类型为wait_quene_head_t的数据结构,
                    struct _ _wait_quene_head{
                        apinlock_t lock; #自旋锁实现同步功能
                        struct list_head task_list;#task_list字段是等待进程链表的头。
                    }
                    typedef struct _ _wait_quene_head_t wait_quene_head_t;
    3)等待队列的元素:元素类型是wait_quene_t;
                    struct _ _wait_quene{
                        unsigned int flags;#flags=1,表示互斥进程(由内核有选择的唤醒) flags=0,表示非互斥进程(由内核在事
                        件发生时唤醒);
                        struct task_struct *task ;#存放描述符地址
                        wait_quene_func_t func;#队列中的进程应该以何种方式唤醒
                        struct list_head task_list;包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中。
                    }
                    typedef struct _ _wait_quene_t wait_quene_t;
    4)等待队列的操作:
        DECLEAR_WAIT_QUENE_HEAD(name)宏定义一个新等待队列头,他静态的声明一个叫name的等待队列头文件并初始化lock和task_list字段。
        init_waitquene_head:用来初始化动态分配的等待队列的头变量。
        init_waitquene_entry(q,p):初始化wait_quene_t结构的变量q。
        DEFINE_WAIT:宏声明一个wait_quene_t类型的新变量。
        sleep_on():该函数吧进程的状态设置为TASK_UNINTERRUPTIABLE,并把他插入到特定的等待队列。然后,它调用调度程序,调度程序重新开始执行一个新的程序。当睡眠被唤醒,调度程序重新开始执行sleep_on()函数,把进程从等待队列中去除。
     注意:1)slep_on函数在以下情况不能使用:那就是必须测试条件并且条件还没有验证就紧接着让进程去睡眠,由于这种条件
是众所周知的竞争条件产生的根源,所以不鼓励这么做。
           2)为了把互斥进程插入等待队列,内核必须使用prepare_to_wait_exclusivea()函数或者使用直接调用add_wait_quene_exclusive()其他所有的相关函数把进程当作非互斥进程来插入。
           3)除非DEFINE_WAIT或者finsh_wait,否则内核必须在唤醒等待进程之后从等待队列中产出对应的等待队列元素。
           
7.进程切换:为了控制进程的执行,内核必须有能力挂起正在cpu上运行的进程,并恢复以前挂起的某个进程的执行。这种行为称为进程切换(process switch)、任务切换(task switch)或者上下文切换(context switch)
    1)硬件上下文:尽管每个进程都有自己的地址空间,但所有进程都必须共享cpu寄存器,因此在恢复一个进程之前,内核必须确保每个寄存器装入了进程挂起时的值,因此,进程恢复之前必须装入寄存器的一组数据叫做硬件上下文。硬件上下文是进程上下文的子集进程上下文包含进程执行的所有信息。硬件上下文一部分在TSS段,而剩余的部分放在内核态。
    2)进程切换的理解:假定prev局部变量表示切换出的进程的描述符,next为切换进的进程描述符。那么进程切换是这样的行为:保存prev硬件上下文,用next的硬件上下文代替prev。因为进程切换经常发生,因此减少保存和装入硬件上下文的花费时间是有必要的。
    3)进程切换只发生在内核态。在执行进程切换之前,用户态使用的所有的寄存器内容都保存在了内核堆栈上,这页包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。
    4)任务状态段:系统中为每个cpu创建一个TSS的原因:
        a.当80x86的一个cpu从用户态切换到内核态时,他就从TSS获取内核态堆栈的地址。
        b.当用户使用in或out指令访问一个i/o端口时,cpu需要访问存在TSS中的i/o许可位图以检查此进程是否有访问端口的权力。
    每个TSS都有一个8字节的状态描述符(TSSD),这个描述符指向TSS起始地址的32位Base字段,20位Limit字段。TSSD的S标志位被清零,表示相应的TSS是系统段的事实。Type置为11或9以表示这个段实际上时一个TTS。linux的TSSD存放在GDT中,GDT的基地址存放在每个cpu的gdtr寄存器中。每个cpu的tr寄存器包含相应的TSS的TSSD选择符,也包含二个隐藏的非编程字段:TSSD的Base字段和Limit字段,这样处理器可以直接对TSS寻址,而不用从GDT中检索TSS的地址。
    5)thread字段:每个进程描述符包含一个thread_struct类型的thread字段,只要进程切换出去,内核的硬件上下文就会存储在这个结构中。这个结构包含的字段涉及大部分的cpu寄存器,但不包含eax,ebx等这些通用寄存器。
    6)执行进程切换,由二步组成:
        a.切换页全局目录以安装一个新的地址空间;
        b.切换内核态堆栈和硬件上下文,因为硬件上下文提供内核执行新进程所需要的所有信息,包括cpu寄存器。(由switch_to宏实现)
    7)switch_to宏和_ _switch_to()函数 
8.进程三部曲之创建
     linux中,第一个进程是固定的.与生俱来的或者说是由内核设计者安排好的,内核引导并完成基本初始化后,就有系统的第一个进程(实际上是内核线程)。
     所有其他的进程都是由这个原始进程或者其子孙进程所创建。linux中一个新的进程一定是已经存在的进程“复制”出来的,而不是创造出来的。
    进程创建的二步走:
    linux将进程的创建和目标程序的执行分二部分。第一步是从已经存在的“父进程”中复制出具有自己的task_struct结构和系统空间堆栈,并与父进程共享其他所有资源的子进程。例如,父进程打开了五个文件,那子进程也会打开五个文件,而且这些文件当前读写指针也停在相同的地方。这一步可以“复制”。一般linux为此提供二种系统调用,分别是fork()和clone()。
    系统调用fork() vfork()与clone()------》他们都会调用与体系无关的do_fork函数;
    区别在于fork()是全部复制,父进程多有的资源全部通过数据结构的复制“遗传”给子进程,而clone()则可以是有选择的复制父进程的资源,不通过复制数据结构而是通过复制指针让子系统共享,从而创建一个进程,此外,clone的作用主要是创建一个线程,可以是内核线程,也可以是用户线程(可以设定子进程的用户空间堆栈的的位置,还可以制定用户空间的运行起点)。系统调用vfork(),作用也是创建一个线程,不带参数,主要作为创建进程的中间步骤,其除了task_struct结构和系统空间堆栈以外的资源,全是通过数据结构指针复制遗传的,主要出于效率考虑,程序设计接口和fork相同。
    第二步是目标程序的执行,创建新的进程的目的就是有不同的目标程序需要去执行,所以复制结束后,子进程需要走自己的路,去执行自己的程序,linux专为此提供了系统调用execve(),让一个进程以文件形式存在的一个可执行程序的映像。
    敲黑板:内核线程不可以像进程一样执行可执行映像文件,只能执行内核中的一个函数,这种执行目标程序方式的不同,引起了一个重要的不同,那就是进程调用了exevc()之后不在返回,而是“客死他乡”,在所执行的程序中死去,而内核线程是调用一个目标函数,当然要从那个函数返回。

      子进程与父进程的微妙区别:pid不同;从fork()返回时所具有的返回值不一样,子进程返回的是0,父进程返回的是子进程的pid。注意,在使用clone()来创建子进程时,父进程可以具有和子进程相同的pid,此时区分它们通过比较堆栈指针更为可靠,这种方法也只适用于内核线程,因为普通的进程都在用户空间,根本不知道系统空间堆栈在那里。
    子进程创建后父进程的归宿:第一,走自己的路 第二,停下来,进入睡眠状态,等待子进程完成使命而最终去世,然后继续执行linux提供wait3()[等待任何一个子进程去世]和wait4()(等特定的子进程去世)的系统调用。第三,自行结束自己的生命,系统调用exit(),和第一种类似,是特例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值