Linux之进程管理详解

1 什么是进程

       进程就是处于执行期的程序(目标码存放在某种存储介质上)。通常进程还要包含其他资源,例如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个线程。还有存放全局变量的数据段,实际上进程就是执行的程序代码的实时结果。【Linux内核设计与实现】

2 怎么创建进程

       在Linux系统中 通常是fork()系统调用(fork()实际上有clone()系统调用实现的)的结果,在系统调用通过复制一个现有的进程来创建一个新的进程,调用fork()系统调用的称为父进程,新产生的进程称为子进程。fork() 系统调用从内核返回两次,一次回到父进程,另外一次回到新产生的子进程中。子进程可以紧接着调用exec()这组函数就可以创建新的地址空间,将新的程序载入其中。这样父子进程就可以并行进行运行了。

3 进程描述符及任务结构

        Linux内核通常也把进程叫做任务。 内核把进程的列表存放在任务队列的双向循环列表中。列表的每一项都是类型为task_struct 称为进程描述符。 进程描述符包含的数据能够完整的描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有更多其他的信息。

//进程描述符
struct task_struct{
  unsigned long state; //进程状态
  int prio ;
  unsigned long policy;
  struct task_struct *parent;
  struct list_head tasks;
  pid_t pid; //进程PID
  ...
}

       分配进程描述符:Linux通过slab分配器分配task_struct结构,这样能够达到对象的复用和缓存着色。 在2.6以前的内核,各个进程的task_struct存放在他们的内核栈的尾端,现在由于使用slab分配器生成task_struct,所以只需在栈底或者栈顶(由栈的出口决定)创建一个新的结构 struct_thread_info

struct thread_info {

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

每一个任务的thread_info结构都在它的内核栈的尾端分配。

       进程描述符的存放:内核通过一个唯一的进程标识值(PID)来标识每一个进程。PID的最大默认值为32768 内核把每个进程的PID是存放在task_struct进程描述符中。 如果要修改PID的默认最大值的话,可以由系统管理员通过修改 /proc/sys/kernel/pid_max来提高上限。在内核中,访问任务都必须获得指向其task_struct结构的指针,可以通过current宏进行查到正在运行的进程描述符。

4 进程的状态

       进程描述符中的state域描述了当前进程的状态,进程一共有五种状态,系统中的每一个进程都必然处于五种状态中的一种

.TASK_RUNNING(运行): 进程是可执行的:它或者正在执行,或者在运行队列中等待执行。
   这是进程在用户空间执行的唯一可能的状态

.TASK_INTERRUPTIBLE (可中断):进程正在睡眠(也就是说进程阻塞)等待某些条件的达成

.TASK_UNINTERRUPTIBLE (不可中断)

.__TASK_TRACED(被其他进程跟踪的进程)

.__TASK__STOPPED(停止)

设置进程的状态: 使用set_task_state(task, state)函数 该函数将指定的进程设置为指定的状态,必要的时候,它会设置内存屏障来强制其他处理器做重新排序

5 进程家族树

     Unix系统的进程之前存在一个明显的继承关系,Linux系统也是如此,所有的进程是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动整个过程,系统中的每一个进程都有父进程,相应的每一个进程都有0个或者多个子进程。拥有同一个父进程的所有进程称为兄弟,进程间的关系也存在了进程描述符中,每个task_struct结构都包含一个parent指针,还有一个称为children的子进程链表。由于所有的进程描述符都存放在任务队列中,任务队列是一个双向的循环列表,因此可以通过遍历列表来遍历所有的进程,也可以通过继承体系来遍历所有的进程。

6 进程创建

     Unix的进程创建很特别,许多其他的操作系统都提供了产生spawn进程的机制。首先创建新的地址空间,创建进程,读入可执行的文件,最后开始执行。 Unix采用了不同的实现方式,它把上述步骤分解成了两个单独的函数中去执行,分别是fork()和exec()(exec在这里指的是一族的函数),首先fork()通过拷贝当前进程创建一个子进程,exec()负责读取可执行的文件并载入到地址空间开始运行。

     写时拷贝:传统的fork()系统调用直接将父进程的所有资源都拷贝给子进程,这种实现效率特别低下,例如,如果新的进程立即执行新的程序,拷贝就没有任务意义。因此Linux的fork()系统调用采用了写时拷贝(copy-on-write)实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不需要复制整个进程内存空间,而是让父进程和子进程共享一个拷贝。只有在写入的时候,数据才会被复制,从而使得各个进程拥有各自的拷贝。也就是说资源的拷贝只有在需要写入的时候才会进行,在此之前,只是以只读的方式共享看。 fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符,在一般的情况下,进程创建之后都会立马运行一个可执行文件,因此写时拷贝这种技术可以避免拷贝大量不会被使用的数据。

     fork(): Linux系统通过clone()系统调用来实现fork(), 通过一系列的参数标志来指明父子进程需要共享的资源。 fork() , vfork(),和__clone()库函数都根据各自需要的参数标志去调用的clone(),然后由clone调用的do_fork(), do_fork()完成了创建的大部分工作,它定义在kernel/fork.c文件中,该函数调用copy_process()函数  因此创建进程的整个流程就是fork()---->clone()------>do_fork()----->copy_process() 参数就传入所需共享的资源标识。整个创建进程的流程如下

1)调用dup_task_struct()为新进程创建一个内核栈,thread_info以及task_struct进程描述符,这些值当前是与父进程完成相同的

2)检查并确保新创建的这个子进程,当前用户所拥有的进程数目没有超出给他分配的资源限制

3)子进程着手开始使自己和父进程区别开来。进程描述符内的许多成员都要被清0或者设置为初始值(那些不是继承而来的进程描述符成员,主要是统计信息)task_struct中的大多数数据都依然未被修改

4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会被投入

5)copy_process()调用copy_flags()以更新task_struct的flags成员表明拥有超级权限的标志被清0,表明进程还没有调用exec()函数的标志被设置

6)调用alloc_pid()分配一个新的有效的PID

7)根据传入的参数标志 copy_process()拷贝或共共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间。在一般的情况下,这个资源会被给定进程的所有线程共享

8)最后,copy_process()返回一个指向子进程的指针。

     vfork(): 除了不拷贝父进程的页表项外,vfork()系统调用和fork()系统调用功能相同,子进程作为父进程的一个单独的线程在它的地址空间运行,父进程被阻塞,直到子进程退出或者执行exec(),vfork()系统调用通过向clone()函数传递一个特殊的标志来进行的

1)在调用copy_process()时 task_struct的vfork_done成员被设置为NULL

2) 在执行do_fork(),如果给定特别的标志,则vfork_done则会指向一个特定的地址

3)子进程开始先执行后,父进程不是马上恢复,而是一直等待,知道子进程通过vfork_done指针向它发信号

4)在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done指针是否为空,如果不为空,则向父进程发送信号

5)回到do_fork()父进程醒来并返回。如果一切执行顺利的话,子进程在新的地址空间里运行而父进程也恢复在原地址空间的运行,开销虽然降低了,但是实现并不是最优良的。

fork()和vfork()的区别:

1)fork(): 父子进程的执行次序不确定。

vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。

2)fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。

vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的)

7 线程的创建

         Linux线程的实现机制非常独特,内核并没有准备特别的调度算法或者定义线程的数据结构。

相反,线程仅仅是被视为一个与其他进程共享某些资源的进程,每一个线程也有自己的task_struct,因此在Linux系统中线程也是一个普通的进程。

        线程的创建与普通进程的创建类似,只是在调用clone()系统调用时,传入的一些参数标识来表明要共享的资源    

clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGCHID, 0)

从调用clone()传入的参数看,可以知道,线程是共享地址空间,文件系统资源,文件描述符,信号处理程序的。

一个普通fork()实现是

clone(SIGGHLD,0);

   vfork()实现是:

clone(CLONE_VFORK | CLONE_VM | SIGGHLD,0);

内核线程:内核线程和普通线程的区别在于内核线程没有独立的地址空间(实际上指向内存地址空间的mm指针是NULL) 它们只能在内核空间运行,从来不切换到用户空间。

8 进程终结

   当一个进程终结时,内核必须释放它所占用的资源,并且通知父进程,一般来说,进程的析构是自身引起的,

1)调用exit()

2)隐式的从某一个程序的主函数返回main函数(main函数的返回点放置了exit()的代码),

3)接受到一个它既不能处理也不能忽略的信号或者异常时 也会被动终结

进程终结是调用do_exit()系统调用完成的:子进程退出时,会调用exit_notify()向父进程发送信号,给子进程重新找养父,在调用了do_exit()系统调用后,尽管线程已经将死不能再运行,但是系统还是保留了它的进程描述符,父进程可以通过进程描述符获取已经僵死的子进程的信息,然后通知内核它不关注这些信息后,子进程的进程描述符被释放。父进程可以调用wait()系统调用来处理僵死的子进程(僵尸进程)

僵尸进程: 一个父进程利用fork创建子进程,如果子进程退出,而父进程没有利用wait 或者  waitpid 来获取子进程的状态信息,那么子进程的状态描述符依然保存在系统中。

孤儿进程:一个父进程退出, 而它的一个或几个子进程仍然还在运行,那么这些子进程就会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集的工作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值