【Linux系统】进程管理

来源:互联网 发布:stats星际知乎 编辑:程序博客网 时间:2024/06/11 21:14

一.进程的定义和相关的概念

  • 进程就是出于执行期的程序和相关资源。包含:代码段、数据段、打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程。线程是进程中活动的对象。每个线程拥有独立的程序计数器、进程栈、一组进程寄存器。

  • 操作系统提供两种虚拟机制:
    1.虚拟处理器:让进程觉得自己在独享处理器
    2.虚拟内存:让进程在分配和管理内存时觉得自己拥有整个系统的内存资源
    注意:同一个进程中的多线程可以共享虚拟内存,但每个都拥有各自的虚拟处理器

  • 进程的创建
    fork()系统调用:通过复制一个现有进程来创建一个全新的进程。
    fork系统调用从内核返回两次,一次返回到父进程,一次返回到新产生的子进程。
    fork由clone()系统调用实现。
    fork之后调用exec这组函数可以创建新的地址空间,并把新的程序载入其中。程序通过exit()系统调用退出执行,这个函数会将进程占用的资源释放掉,父进程可以通过wait4()系统调用来查询子进程是否终结。
    Linux内核通常将进程叫做任务(task)

  • 进程描述符及任务结构
    内核把进程的列表存放在任务队列双向循环链表中,链表中的每一项都是类型为task_struct(进程描述符)的结构,进程描述符包含了一个具体进程的所有信息。task_struct相对较大,32位机器上占1.7KB,
    这里写图片描述

  • Linux如何分配进程描述符
    Linux通过slab分配器分配task_struct结构,这样能达到对象复用缓存着色的目的。因此task_struct不在内核栈中,而是由slab动态分配的。由于现在用slab分配器动态生成task_struct,所以只需要在栈底创建一个新的结构struct thread_info(可以将这个称为小型进程描述符),这块内核栈区域大小为8KB(两个页)或者4KB。为什么引入thread_info结构:这个结构使计算其偏移变得非常容易。
    这里写图片描述
    从上图可知,内核栈是从该内存区域的顶层向下(从高地址到低地址)增长的,而thread_info结构则是从该区域的开始处向上(从低地址到高地址)增长。内核栈的栈顶地址存储在esp寄存器中。所以,当进程从用户态切换到内核态后,esp寄存器指向这个区域的末端。
    每个任务的thread_info结构在它的内核栈的尾端分配。
    结构体中task指向实际的task_struct的指针。

  • 进程描述符的存放
    内核用唯一的PID来标识每个进程,为pid_t类型,默认最大值32768,内核把每个进程的PID放入他们各自的进程描述符中。
    通过屏蔽掉esp寄存器中的内核栈顶地址的低13位(或12位,当THREAD_SIZE为4096时)可以找到当前进程thread_info结构。此时所指的地址就是这片内存区域的起始地址,也就刚好是thread_info结构的地址.

  • 进程状态:
    TASK_RUNNING:正在执行或者在运行队列中等待执行。
    TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞),一旦被唤醒,进程状态将会被设置为TASK_RUNNING
    TASK_UNINTERRUPTIBLE(不可中断):就算收到信号也不会被唤醒或被投入运行。
    这里写图片描述

  • 进程上下文
    系统调用和异常处理是对内核明确定义的接口(程序通过系统调用和异常处理程序从用户空间转向内核空间)

  • 进程家族树
    通过进程描述符的parent指针和children子进程链表实现。

二. 进程创建

  • 三个函数
    fork():拷贝当前进程创建子进程。
    exec():负责读取可执行文件并将其载入地址空间开始运行。
    vfork():子进程和父进程共享数据段;保证父进程挂起,子进程先运行;在子进程调用exec或exit之后父进程才有机会运行。

  • 写时拷贝:子进程创建时与父进程以只读方式共享数据,只有在资源的复制中需要写入时才生成拷贝。比如这种优化对fork之后调用exec函数的作用特别明显

  • 当进程创建时,每个进程都会有一个自己的 4GB 虚拟地址空间。要注意的是这个 4GB 的地址空间是“虚拟”的,并不是真实存在的,而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。

三. Linux线程的实现:

  • 内核调度对象是线程,不是进程。对Linux而言,线程只不过是一种特殊的进程。Linux实现线程通过仅仅创建了进程并分配通用的task_struct结构,在建立进程时指定他们共享某些资源。

  • clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0 );
    共享虚拟地址空间,文件系统资源(包括root、当前目录、umask),文件描述符,信号处理程序。仅调用clone时传入的参数和fork不同。包括共享虚拟地址空间,文件系统资源(包括root、当前目录、umask),文件描述符,信号处理程序。

  • 参数说明:
    CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
    CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
    CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
    CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
    CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
    CLONE_PTRACE 若父进程被trace,子进程也被trace
    CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
    CLONE_VM 子进程与父进程运行于相同的内存空间
    CLONE_PID 子进程在创建时PID与父进程一致
    CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

  • 内核线程:独立运行在内核空间的标准进程,没有独立的地址空间(mm指针为NULL)
    内核地址空间划分
    这里写图片描述

  • 进程终结
    调用do_exit后,与进程相关联的所有资源都被释放掉了并处于EXIT_ZOMBIE状态,此时它占用的内存只有内核栈、thread_info结构、task_struct结构。此时进程存在的唯一目的是向父进程提供信息,父进程检索到信息后,由进程把剩余内存释放。
    进程终结所需的清理工作和进程描述的删除分开执行,wait()函数会调用realease_task()来释放内核栈、thread_info结构和task_struct结构。

  • 孤儿进程
    当子进程还未退出之前父进程先退出,就要给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父亲。

0 0