Linux的进程管理

目录

1、概述

2、进程描述符

2.1 进程描述符的分配

2.2 进程描述符的存放

2.3 进程状态

2.4 进程上下文

2.5 进程家族树

3、进程的创建

4、进程的终结

5、线程的实现


1、概述

    进程是执行期的代码。但是进程不止包括这样一段可执行的代码,还包括进程执行相关的各种资源:

  • 打开的文件;
  • 挂起的信号;
  • 内核内部的数据;
  • 处理器状态;
  • 一个或多个具有内存映射的地址空间;
  • 一个或多个执行线程;
  • 存放全局变量的数据段;
  • ......

    进程在创建它的时候开始存活,在Linux系统中,这通常的fork()系统调用的结果。fork()系统调用通过复制一个现有进程来创建一个新的进程,调用fork()的进程称为父进程,新创建的进程称为子进程。在fork()系统调用结束时,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次是回到父进程,一次是回到子进程。Linux内核中,fork()实际上是通过clone()调用实现的。

    fork()调用后,通过exec()调用可以创建新的地址空间,并把程序载入其中。最终,程序通过exit()调用退出执行,并释放其占用的资源。父进程可以通过wait4()调用等待子进程的执行结果。程序执行完成后被设置为僵死状态,直到它的父进程调用wait()或waitpid()调用。

    执行线程简称线程,是在进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。对于Linux而言,线程就是一种特殊的进程。

2、进程描述符

    进程描述符用来描述一个具体的进程的所有信息。进程描述符的数据结构叫做task_struct,定义在<linux/sched.h>文件中。

    task_struct数据大小约为1.7KB(32位机器),包含了内核管理一个进程所需要的全部信息:

  • 进程的id(pid);
  • 打开的文件;
  • 进程的地址空间;
  • 挂起的信息;
  • 进程的状态;
  • ......

2.1 进程描述符的分配

    Linux通过slab分配器动态分配task_struct结构,分配时只需要在进程内核栈的尾端创建一个新的结构thread_info即可。其中,task域存放的是指向task_struct的指针。

2.2 进程描述符的存放

    内核通过唯一的进程标识值PID(Process Identification Value)来标识每个进程。pid的类型是隐含类型pid_t,实际上就是int。为了兼容老版本,PID数的最大值是32768(有符号short int最大值),但是可以在<linux/threads.h>中增加到400万。内核把每个进程的pid存放到进程描述符中。

    内核大部分处理进程的操作,都是通过task_struct实现的,因此内核需要能获取到指向task_struct的指针。有些内存结构有专门的寄存器存放task_struct的指针,而x86等寄存器不富裕的体系,只能在内核栈的尾端创建thread_info,通过计算内存偏移来简接查找task_struct。

2.3 进程状态

    进程描述符的state域描述了进程的状态。进程包含5种状态:

  • TASK_RUNNING:运行,进程是可执行的。可能正在执行,也可能正在任务队列等待执行。这是进程在用户空间中执行的唯一可能的状态;
  • TASK_INTERRUPTIBLE:可中断,此时的进程正在睡眠(也叫阻塞),等待某些条件的达成。处于此状态的进程会因为提前接收到信号而被唤醒;
  • TASK_UNINTERRUPTIBLE:不可中断,除了接收到信号也不会提前被唤醒,其他的和可中断相同。这个状态通常用在等待时不受干扰,或者等待的事情很快发生的场景;
  • __TASK_TRACED:被其他进程跟踪的进程,例如通过ptrace对调试进程进行跟踪;
  • __TASK_STOPPED:停止,进程停止执行,进程没有投入运行,也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时;在调试期间接受到任何信号,也会进入这个状态。

2.4 进程上下文

    当一个程序执行了系统调用,或者触发了某个异常,就陷入了内核空间,此时称内核“代表程序执行”,并处于进程上下文。除非期间有更高优先级的进程要执行,并且调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续运行。

    系统调用和异常处理是对内核明确定义的接口,进程只有通过这些接口才能陷入内核,即进程对内核的所有访问都需要通过这些接口。

2.5 进程家族树

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

    系统中每个进程都有一个父进程,和零个或多个子进程。拥有同一父进程的进程,称为兄弟进程。进程间的关系存放在进程描述符中,每个task_struct都包含一个parent指针,指向父进程所在的task_struct,以及一个名为children的指针链表,指向所有的子进程的task_struct。

3、进程的创建

    Unix把进程的创建拆分成了两个函数:

  • fork():Linux通过clone()系统调用实现了fork(),通过拷贝当前进程创建一个子进程。子进程和父进程的区别只在于PID、PPID(父进程的PID)和一些统计量。
  • exec():将可执行文件载入地址空间开始执行。

    Linux的fork()使用写时拷贝(copy-on-write)页实现资源的复制。资源的复制,只有在需要写入的时候才进行,在此之前以只读方式共享。这种技术使地址空间的页拷贝推迟到实际发生写入的时候才进行,如果页根本不会被写入,例如fork()后立即执行exec(),则无需复制。这样可以避免拷贝大量根本不会被使用的数据,这也是Unix快速执行的能力的一种重要支撑。

    fork()的实际开销就是复制父进程的页表,以及给子进程创建唯一的进程描述符。进程页表是进程私有的结果,映射了内核态和用户态的线性地址。

4、进程的终结

    当一个进程终结时,内核会回收进程的全部资源,并通知父进程。

1、do_exit()方法回收资源并更新状态:

  1. 回收地址空间:如果没有其他进程占用,就释放掉地址空间;
  2. 回收文件描述符、文件系统数据:引用计数分别递减,如果减为0,就释放;
  3. 执行退出代码:执行操作系统规定的退出动作,并把退出代码存放到task_struct中的exit_code中供父进程随时检索;
  4. 设置退出状态:task_struct中的exit_state设置为EXIT_ZOMBIE,进程不再接受调度;
  5. 给子进程重新寻找父进程,新的父进程是线程组的其他线程,或者init进程;
  6. 调用schedule()方法切换到新的进程。

2、此时,进程所占有的内存资源只剩下内核栈、thread_info和task_struct。此时进程存在的唯一目的,就是向它的父进程提供信息。父进程检索到相关信息后,或者通知内核那是无关信息后,内核会释放剩下的这些资源。

5、线程的实现

    Linux内核并没有线程的概念,它把线程当做进程来实现,线程仅仅被视为与其他进程共享某些资源的进程。线程有自己的task_struct,所以从内核的角度,线程只是一个普通的进程,仅仅是和其他进程共享了一些资源,比如地址空间等。

1、线程的创建

    创建线程和创建进程相似,也是调用clone()实现,只不过传递的一些参数,用来控制共享一些资源:

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

    这些代码是用来和父进程共享:地址空间、文件系统资源、文件描述符和信号处理程序

    相对之下,创建进程的命令:

clone(SIGCHLD)

2、内核线程

    内核线程是独立运行在内核空间的标准进程,内核在后台执行操作,使用内核线程来完成。内核线程和普通进程的区别,只在于内核线程没有独立的地址空间。它们只在内核空间运行,不会切换到用户空间。内核进程和普通进程一样,可以被调度,也可以被抢占。

    内核线程只能由内核线程来创建,内核从kthreadadd内核进程衍生出新的内核进程。在Linux系统中,可以使用ps -ef命令看到很多内核线程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值