Linux内核-进程管理

引言

本文主要介绍Linux内核进程管理相关知识,包括进程描述符、进程创建、销毁、状态、线程的实现以及Linux进程相关命令等。

进程描述符

内核把进程的列表存放在叫做任务队列( task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符的结构,进程描述符中包含一个具体进程的所有信息。
task_struct包含了内核管理一个进程所需的所有信息,能完整地描述一-个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息。

进程描述符分配与存放

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

struct thread_info {struct task_struct   *task;      /* main task structure */__u32          flags;      /* low level flags */__u32          status;     /* thread synchronous flags */__u32         cpu;        /* current CPU */mm_segment_t       addr_limit;unsigned int     sig_on_uaccess_error:1;unsigned int     uaccess_err:1;  /* uaccess failed */
};

在这个图中,esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。

为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task。

进程状态

  • TASK_RUNNING:可运行状态,处于该状态的进程可以被调度执行而成为当前进程.
  • TASK_INTERRUPTIBLE:可中断睡眠状态,处于该状态的进程在所需资源有效时被唤醒,也可以通过信号或者定时中断唤醒.
  • TASK_UNINTERRUPTIBLE:不可中断睡眠状态,处于该状态的进程等待时不受干扰,就算接收到信号也不会被唤醒。
  • TASK_ZOMBLE:当子进程退出时,父进程没有回收子进程的资源,这是子进程就会进入僵尸态。
  • TASK_STOPPED:进程停止执行,进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU时,进程会进入停止态,向进程发送SIGCONT可让进入进入运行态。进入停止态的进程不会被调度器调度。

可以使用set_task_state(task, state)函数调整某个进程的状态。

进程关系

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

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

进程创建

许多其他的操作系统产生进程的机制是首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix 采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。**exec()函数负责读取可执行文件并将其载入地址空间开始运行。**把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。

写时拷贝

传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux 的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。

只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,**资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。**这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。

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

fork()

Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。
do_fork完成了创建中的大部分工作,它的定义在kernelfork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:
1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
2〉检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
3)子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清О或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。task_struct中的大多数数据都依然未被修改。
4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。

5)copy_process()调用copy_flags()以更新task_struct 的flags成员。表明进程是否拥有超级用户权限的 PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
6)调用alloc _pid()为新进程分配一个有效的PID。
7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

8)最后,copy _process(做扫尾工作并返回一个指向子进程的指针。
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

进程终止

一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回(其实C语言编译器会在main)函数的返回点后面放置调用exit()的代码)。当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动地终结。不管进程是怎么终结的,该任务大部分都要靠do_exit()(定义于kernelexit.c)来完成,它要做下面这些烦琐的工作:

  1. 将tast_struct中的标志成员设置为PF_EXITING。
  2. 调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。
  4. 然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
  5. 接下来调用sem _exit()函数。如果进程排队等候IPC信号,它则离开队列。
  6. 调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
  7. 接着把存放在.task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
  8. 调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。
  9. do_exit(调用schedule()切换到新的进程(参看第4章)。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。

值得关注的是do_exit()函数释放了进程所占用的资源,包括释放占用的内存空间、将进程从信号等待队列里去除,递减进程所引用的文件描述符、文件系统数据的引用数据;但是还有进程的内核栈、thread_info和task_struct结构体没有释放,此时进程处在僵尸态,存在的唯一目的就是向父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。

在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。
当最终需要释放进程描述符时,**release_task()**会被调用,用以完成以下工作:

  1. 它调用_exit_signal),该函数调用_unhash_process(),后者又调用detach_pid() 从pidhash上删除该进程,同时也要从任务列表中删除该进程。
  1. _exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
  2. 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。
  3. release_task()调用put_task_struct()释放进程内核栈和 thread_info结构所占的页,并释放tast_struct所占的slab高速缓存。

看可以看出进程终结时所需的清理工作和进程描述符的删除被分开执行。

线程的实现

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

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

**新创建的进程与父进程共享地址空间、文件系统资源、文件描述符和信号处理程序。**换个说法就是,新建的进程和它的父进程就是所谓的线程。

ps命令

ps是linux下最常用的也是非常强大的进程查看命令,常配合管道命令 | 和查找命令 grep 同时执行来查看特定进程。

选项 功能
-A 显示所有的进程,跟-e的效果相同
-a 显示现行终端机下的所有进程,包括其他用户的进程
-u 显示当前用户的进程状态
-x 通常与 a 这个参数一起使用,可列出较完整信息
-l 较长、较详细的将该PID的信息列出
-j 工作的格式(jobs format)
-f 把进程的所有信息都显示出来
-e 表示显示所有继承

ps命令参数较多,只需要记住几个常见的组合即可。

  • ps -aux

    USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root           1  0.0  0.3 259884  5964 ?        Ss   May04   5:09 /usr/lib/systemd/systemd --swi
    root           2  0.0  0.0      0     0 ?        S    May04   0:01 [kthreadd]
    root           3  0.0  0.0      0     0 ?        I<   May04   0:00 [rcu_gp]
    root           4  0.0  0.0      0     0 ?        I<   May04   0:00 [rcu_par_gp]
    root           6  0.0  0.0      0     0 ?        I<   May04   0:00 [kworker/0:0H-events_highpri]
    root           9  0.0  0.0      0     0 ?        I<   May04   0:00 [mm_percpu_wq]
    root          10  0.0  0.0      0     0 ?        S    May04   6:15 [ksoftirqd/0]
    root          11  0.0  0.0      0     0 ?        R    May04  16:01 [rcu_sched]
    root          12  0.0  0.0      0     0 ?        S    May04   0:00 [migration/0]
    root          13  0.0  0.0      0     0 ?        S    May04   0:00 [watchdog/0]
    root          14  0.0  0.0      0     0 ?        S    May04   0:00 [cpuhp/0]
    ..................

    USER 行程拥有者
    PID 进程的ID
    %CPU 占用的 CPU 使用率
    %MEM 占用的记忆体使用率
    VSZ 占用的虚拟记忆体大小
    RSS 占用的记忆体大小
    TTY 终端的次要装置号码 (minor device number of tty)
    STAT 该行程的状态
    START 行程开始时间
    TIME 执行的时间
    COMMAND 所执行的指令

  • ps -ef

    UID          PID    PPID  C STIME TTY          TIME CMD
    root           1       0  0 May04 ?        00:05:09 /usr/lib/systemd/systemd --switched-root --sy
    root           2       0  0 May04 ?        00:00:01 [kthreadd]
    root           3       2  0 May04 ?        00:00:00 [rcu_gp]
    root           4       2  0 May04 ?        00:00:00 [rcu_par_gp]
    root           6       2  0 May04 ?        00:00:00 [kworker/0:0H-events_highpri]
    root           9       2  0 May04 ?        00:00:00 [mm_percpu_wq]
    root          10       2  0 May04 ?        00:06:15 [ksoftirqd/0]
    root          11       2  0 May04 ?        00:16:01 [rcu_sched]

    UID 用户的ID ,但输出的是用户名
    PID 进程的ID
    PPID 父进程的ID
    C 进程占用CPU的百分比
    STIME 进程启用到现在的时间
    TIME 该进程实际使用CUP运行的时间
    TTY 该进程在哪个终端上运行,若与终端无关,则显示?,若为pts/0等,则表示由网络连接主机进程
    CMD 命令的名称和参数
    两者的输出结果差别不大,但展示风格不同。aux是BSD风格,-ef是System V风格。


参考资料

《Linux内核设计与实现》

https://zhuanlan.zhihu.com/p/112970875

Linux内核-进程管理相关推荐

  1. 挑战360无死角讲解Linux内核 进程管理,调度器的5种实现丨C++后端开发丨C/C++Linux服务器开发丨内核开发丨网络编程

    挑战360无死角讲解 进程管理,调度器的5种实现 1. 8500行 CFS是什么 2. RT调度器使用场景 3. IDLE/Dealine调度器 视频讲解如下,点击观看: 挑战360无死角讲解Linu ...

  2. Linux内核——进程管理与调度

    进程的管理与调度 进程管理 进程描写叙述符及任务结构 进程存放在叫做任务队列(tasklist)的双向循环链表中.链表中的每一项包括一个详细进程的全部信息,类型为task_struct,称为进程描写叙 ...

  3. linux kernel 进程管理,Linux内核 | 进程管理

    1. 进程和线程 1.1 定义 进程是处于运行状态的程序和相关资源的总称,是资源分配的最小单位. 线程是进程的内部的一个执行序列,是CPU调度的最小单位.有一段可执行程序代码. 有一段进程专用的系统堆 ...

  4. linux进程家族树,Linux内核 | 进程管理

    作者:世至其美 博客地址:hqber.com 转载须注明以上信息, 更多文章,请访问个人博客:hqber.com 1. 进程和线程 1.1 定义 进程是处于运行状态的程序和相关资源的总称,是资源分配的 ...

  5. 【Linux 内核 内存管理】虚拟地址空间布局架构 ③ ( 内存描述符 mm_struct 结构体成员分析 | mmap | mm_rb | task_size | pgd | mm_users )

    文章目录 一.mm_struct 结构体成员分析 1.mmap 成员 2.mm_rb 成员 3.get_unmapped_area 函数指针 4.task_size 成员 5.pgd 成员 6.mm_ ...

  6. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  7. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  8. 【Linux 内核 内存管理】内存管理架构 ① ( 内存管理架构组成 | 用户空间 | 内核空间 | MMU 硬件 | Linux 内核架构层次 | Linux 系统调用接口 )

    文章目录 一.内存管理架构组成 ( 用户空间 | 内核空间 | MMU 硬件 ) 二.Linux 内核架构层次 三.Linux 系统调用接口 一.内存管理架构组成 ( 用户空间 | 内核空间 | MM ...

  9. linux进程管理 pdf,高效与精细的结合--Linux的进程管理.pdf

    高效与精细的结合--Linux的进程管理.pdf 第 卷 第 期 A 文献标识码 I T6L 76 28 L J6 7 8 676 LJ Q Q656 8J6 6 82 K 797863 R28J 2 ...

最新文章

  1. 对于mysql存储过程感想_存储过程学习心得
  2. 腾讯数据库专家雷海林分享智能运维架构
  3. HDU4267(2012年长春站)
  4. 网页加载出现没有合适的负载均衡器_分布式必知必会-七层负载和四层负载到底是什么?...
  5. kaggle案例实战班
  6. ASP.NET Web API 2 中的属性路由使用(转载)
  7. 读书APP的不二备胎,我选了网易蜗牛读书
  8. 电脑连手机热点DNS服务器无响应,电脑连接手机热点无法上网解决方法有哪些
  9. 机器学习 第一节 第一课
  10. Windows密钥备份
  11. 微信HOOK 退出群聊
  12. 才发现Nero8出现了问题
  13. Google Colab V100 +TensorFlow1.15.2 性能测试
  14. 第七周PCL学习--点云配准(七)
  15. h264编码流程分析
  16. 使用范例调教ChatGPT
  17. ¥1-1 SWUST oj 941: 有序顺序表的合并操作的实现
  18. Arduino 语法参考
  19. 移民就移民了,别拉祖国来垫背
  20. [绍棠] iOS开发经验总结

热门文章

  1. CAN记录仪 can数据记录仪简介和功能应用 can总线记录
  2. P5.js之数组使用——绘制水墨画笔,实现跟随鼠标移动的效果
  3. java第六、七章复习
  4. eas 税率修改_EAS委外加工业务操作手册
  5. doris历程_基于 Apache Doris 的小米增长分析平台实践
  6. 东华理工大学南昌校区学计算机,我校学子在2019年第十四届江西省大学生计算机作品赛斩获佳绩...
  7. 如何让RedFlag出声?
  8. 豆浆!——骑上广东第一峰看看日出
  9. 看似“佛系”的手游《QQ炫舞》,背后的音频技术一点都不简单
  10. HTC VIVE TouchPad简单方向控制