Linux内核设计与实现——进程管理

 

Linux内核设计与实现——进程管理

进程

  • 进程就是处于执行期的程序,但不仅仅是可执行程序代码,通常还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,以及用来存放全局变量的数据段等
  • 进程就是正在执行的程序代码的实时结果
  • 执行线程,简称线程(thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。传统UNIX系统中,一个进程只包含一个线程。而Linux对线程和进程并不特别区分,线程只不过是一种特殊的进程罢了
  • 进程提供两种虚拟机制:虚拟处理器和虚拟内存
  • 同一进程中的线程之间可以共享虚拟内存,但每个都拥有自己的虚拟处理器
  • 进程在创建它的时刻开始存活
  • Linux系统中,通过调用fork()复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程
  • 通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。现代Linux内核中,fork()实际上是由clone()系统调用实现的
  • 最终,程序通过exit()系统调用推出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程推出执行后被设置为僵死状态,知道他的父进程调用wait() or waitpid()

进程描述符及任务结构

  • 任务队列:内核把进程的列表放在叫做任务队列的双向循环链表中
  • 进程描述符:链表中的每一项都是task_struct、称为进程描述符的结构,该结构定义在<linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息
  • 进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息

分配进程描述符

  • Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的

  • 在x86上,struct thread_info在文件<asm/thread_info.h>中定义如下:

    struct thread_info {
        struct task_struct		*task;
        struct exec_domain		*exec_domain;
        __u32					flags;
        __u32					status;
        __u32					cpu;
        int						preempt_count;
        mm_segment_t			addr_limit;
        struct restart_block	restart_block;
        void					*sysenter_return;
        int						uaccess_err;
    };
    

    在这里插入图片描述

进程描述符的存放

  • 内核通过一个唯一的进程标识符或PID来标识每个进程。最大值默认为32768,实际上就是系统允许同时存在的进程的最大数目。可通过修改/proc/sys/kernel/pid_max来提高上限
  • 内核中,访问任务通常需要获得指向其task_struct的指针,大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就十分重要
  • 硬件体系不同,该宏的实现也不同,有的可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。而有些像x86这样寄存器不富裕的体系,只能在内核栈的尾端创建thread_info结构,通过计算便宜间接地查找task_struct结构

进程状态

  • 进程描述符中的state域描述了进程的当前状态
  • 每个进程必然处于五个状态其中之一
    1. TASK_RUNNING(运行):进程是可执行的;或者正在执行,或者在运行队列中等待执行。这是进程在用用户空间中执行的唯一可能的状态
    2. TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞),等待某些条件的达成,然后进入运行态
    3. TASK_UNINTERRUPTIBLE(不可中断):除接受到信号也不会被唤醒或准备运行外,和可中断状态相同。这个状态通常在进程必须在等待是不受干扰或等待时间很快就会发生时出现,使用较少
    4. __TASK_TRACED:被其他进程跟踪的进程
    5. __TASK_STOPPED:进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接受到任何信号,都会使进程进入这种状态

在这里插入图片描述

设置当前进程状态

  • 内核经常需要调整某个进程的状态。使用如下函数

    set_task_state(task,state); /\*将任务task的状态设置为state\*/

  • 该函数将指定的进程设置为指定的状态。必要时,会设置内存屏障来强制其他处理器作重新排序。否则,它等价于:

    task->state = state

  • set_current_state(state)set_task_state(current,state)含义相同

进程上下文

  • 可执行程序代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行
  • 当一个程序执行了系统调用或者触发了某个异常,他就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中
  • 在此上下文中current宏是有效的,除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间会继续执行
  • 系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行:对内核的所有访问都必须通过这些接口

进程家族树

  • 所有的进程都是PID为1的init进程的后代:内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程

  • 进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程task_struct,叫做parent的指针,还包含一个称为children的子进程链表。对于当前进程,可通过下面的代码获得其父进程的进程描述符:

    struct task_strct *my_parent = current->parent;

    同样,也可以按以下方式依次访问子进程:

    struct task_struct *task;
    strunt list_head   *list;
    
    list_for_each(list,&current->children) {
        task = list_entry(list,struct task_struct,sibling);
        /*task现在指向当前的某个子进程*/
    }
    
  • init进程的进程描述符是作为init_task静态分配的

  • 获取链表中下一个进程

    list_entry(task->tasks.next,struct task_struct,tasks)

    获取前一个进程

    list_entry(task->tasks.prev,struct task_struct,tasks)

进程创建

  • 其他操作系统提供产生进程的机制:首先在新的地址空间里创建进程,读入可执行文件,最后开始执行
  • Unix采用了不同的方式,将上述步骤分解到了两个单独的函数中去执行,fork()和exec()
    首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID和PPID(父进程PID)和某些资源和统计量(比如挂起的信号,他没有必要被继承)。
    exec()函数负责读取可执行文件并将其载入滴孩子空间开始运行

写时拷贝

  • 传统的fork()系统调用直接把所有的资源赋值给新创建的进程,实现过于简单并且效率底下
  • linux的fork()使用写时拷贝页实现:写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。资源赋值只有在需要写入的时候才进行,在此之前,只是以制度方式共享
  • fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符

fork()

  • linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()

  • do_fork()完成了创建中的大部分工作,该函数调用copy_process()函数,然后让进程开始运行。
    copy_process()函数完成的工作:

    1. 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的
    2. 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制
    3. 子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清零或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。task_struct中的大多数数据都依然未被修改
    4. 子进程的状态被设置为TASK_UNINTERRUPTIABLE,以保证他不会投入运行
    5. copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清零。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置
    6. 调用alloc_pid()为新进程分配一个有效的PID
    7. 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里
    8. 最后,copy_process()做扫尾工作并返回一个指向子进程的指针

    再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能回想地址空间写入

vfork()

  • 除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同
  • 子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,知道子进程退出或执行exec()。子进程不能向地址空间写入
  • 现在由于在执行fork()时引入了写时拷贝页并且明确了子进程先执行,vfork()的好处就仅限于补考呗父进程的页表项了
  • 理想情况下,系统最好不要调用vfork(),内核也不用实现它;完全可以把vfork()实现成一个fork()

----未完待续

这些天有点偷懒了,好久没更新,接着努力吧!!

 

posted @ 2019-09-06 19:09  Ekkone  阅读(118)  评论(0编辑  收藏  举报