Linux内核进程与线程

Linux内核进程与线程

在这里插入图片描述

欢迎点击链接访问我的博客,了解更多内容!

基本概念

进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不仅仅局限于一段可执行程序代码(Unix称其为代码段,text section),通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明地管理所有细节。

所以说,程序本身并不是进程,进程是处于执行期的程序以及相关资源的总称

进程的两种虚拟机制

  • 虚拟处理器

    虽然实际上可能是 许多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器

  • 虚拟内存

    虚拟内存让进程在分配和管理内存时觉得自己拥有整个 系统的所有内存资源。

进程描述符

内核把进程的列表存放在叫做任务队列(task list©)的双向循环链表中。链表中的每一 项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定义在<linux/ sched.h>文件中。进程描述符中包含一个具体进程的所有信息

进程描述符中包含的数据能完整 地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息

在这里插入图片描述

分配进程描述符(slab+thread_info)

Linux通过slab分配器分配task_struct结构,这样能达到对象复用缓存着色(cache coloring)

在2.6以前的内核中,各个进程的task_struct存放在它们 内核栈的尾端。这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。由于现在用slab分配器动态生成task_ struct,所以只需在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个 新的结构 struct thread_info 。

在这里插入图片描述

在这里插入图片描述

每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任 务实际task_struct的指针。

进程描述符的存放

内核通过一个唯一的进程标识值(process identification value)或PID来标识每个进程。PID是 一个数,表示为pid_t隐含类型,实际上就是一个int类型。为了与老版本的Unix和Linux兼容, PID的最大值默认设置为32768 (short int短整型的最大值,),这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管这个值也可以增加到高达400万 (这受<linux/threads.h>中所定义PID最大值的限制)。内核把每个进程的PID存放在它们各自的进程描述符中。

在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏査找到当前正在运行进程的进程描述符的速度就显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。

  • 有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct 的指针,用于加快访问速度。
  • 像x86这样的体系结构(其寄存器并不富余),就只能在内 核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。

进程状态

进程描述符中的state域描述了进程的当前状态,系统中的每个进程都必然处于五种进程状态中的一种。该域的值也必为下列五种状态标志之一:

  • TASK_RUNNING (运行)— 进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行的唯一可能的状态; 这种状态也可以应用到内核空间中正在执行的进程。
  • TASK_INTERRUPTIBLE(可中断)— 进程正在睡眠(也就是说它被阻塞),等待某些条 件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会 因为接收到信号而提前被唤醒并随时准备投入运行
  • TASK_UNINTERRUPTIBLE(不可中断)— 除了就算是接收到信号也不会被唤醒或准备 投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或 等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断 状态e,使用得较少。
  • _TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
  • _TASK_STOPPED (停止)— 进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP, SIGTTIN, SIGTTOU等信号的时候。此外, 在调试期间接收到任何信号,都会使进程进入这种状态。

在这里插入图片描述

设置当前进程状态

在这里插入图片描述

进程上下文

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

进程家族树

Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此。所有的进程都 是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初 始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个过程

系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥 有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个task_struct都 包含一个指向其父进程tast_struct、叫做parent的指针,还包含一个称为children的子进程链表。 所以。

  • 对于当前进程,通过下面的代码获得其父进程的进程描述符:
struct task_struct *my_parent =  current->parent;
  • 通过以下方式访问子进程

    struct task_struct *task;
    struct list_head *list;
    
    list_for_each(list,&current->children){
        task = list_entry(list,struct task_struct,sibling); //task指向当前的某个子进程
    }
    

在这里插入图片描述

进程创建(fork+exec)

在新的地址空间创建线程,读入可执行文件,最后开始执行。

fork

拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID (每个进程唯一)、PPID (父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。

exec

exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。

写时拷贝

传统的fork()系统调用直接把所有的资源复制给新创建的进程

Linux的fbrk()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。

只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资 源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间 上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例来说, fork。后立即调用exec())它们就无须复制了

fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况 下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。

fork()函数

Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源

fork()、▼fork和_clone () 库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork().

do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_ process()函数,然后让进程开始运行。

**copy_process()**函数流程如下:
在这里插入图片描述
在这里插入图片描述

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

vfork()函数

在这里插入图片描述

线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址 空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计 技术(concurrent programming),在多处理器系统上,它也能保证真正的并行处理(parallelism)。

Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自 己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共 享某些资源,如地址空间)。

上述线程机制的实现与Microsoft Windows或是Sun Solaris等操作系统的实现差异非常 大。这些系统都在内核中提供了专门支持线程的机制(这些系统常常把线程称作轻量级进程 (lightweight processes))o "轻量级进程"这种叫法本身就概括了 Linux在此处与其他系统的差 异。在其他的系统中,相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于**Linux来说,它只是一种进程间共享资源的手段(Linux的进程本身就够轻量级了)°。 举个例子来说,假如我们有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有 一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这 样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建四个进程并分配四个普通的task_struct结构。建立这四个进程时指定他们共享某些资源,这是相当高雅的做法。

创建线程

线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:

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

传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。
在这里插入图片描述
在这里插入图片描述

内核线程

内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread)完成— 独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间***(实际上指向地址空间的mm指针被设置为NULL).它们只在内核空间运行*,从来不切到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。

在装有Linux系统的机子上运行ps -ef命令,你可以看到内核线程,有很多!这些线程在系统启 动时由另外一些内核线程创建。实际上,内核线程也只能由其他内核线程创建。内核是通过从 kthreadd内核进程中衍生出所有新的内核线程来自动处理这一点的。创建的进程处于不可运行状态,如果不通过调用wake_up_process() 明确地唤醒它,它不会主动运行。创建一个进程并让它运行起来,可以通过调用kthread_run() 来达到

在这里插入图片描述

kthread_run() 宏实现

在这里插入图片描述

内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_ stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task struct结构的地址:

int kthread_stop(struct task_struct *k)

进程的终结

当一个进程终结时,内核必须释放它所占有的资源 并把这一不幸告知其父进程。

进程的析构是自身引起的。它发生在进程调用exit()系统调用时,

  • 既可能显式地调用这个系统调用

  • 也可能隐式地从某个程序的主函数返回(其实C语言编译器会在main() 函数的返回点后面放置调用exit()的代码)。

  • 当进程接受到它既不能处理也不能忽略的信号或异 常时,它还可能被动地终结。

    不管进程是怎么终结的,该任务大部分都要靠do_exit()(定义于 kemel/exitx)来完成,

do_exit完成的工作

在这里插入图片描述

的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。

与进程相关联的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE退出状态。它占用的所 有内存就是内核栈、thread_infb结构和tast_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内 存被释放,归还给系统使用。

删除进程描述符

在调用了 do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

在这里插入图片描述

孤儿进程(父进程在子进程之前推出)

如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白地耗费内存。

解决方法给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()中会调用exit_notify(),该函数会调用fbrget_original_parent(),而后者会调用find_new_reaper()来执行寻父过程:

一旦系统为进程成功地找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了。 init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

父进程通过wait()系统调用族来收集其后代信息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值