深入理解Linux内核进程的创建、调度和终止(上)

简介: 深入理解Linux内核进程的创建、调度和终止

Linux内核进程是在Linux操作系统中运行的一种特殊类型的进程。它们与用户空间进程不同,内核进程运行在内核态,并负责执行操作系统的关键功能和任务。这些功能包括设备驱动程序管理、内存管理、文件系统管理、调度等。内核进程通常以kernel或k开头命名,并拥有较高的权限和特权,可以直接访问系统资源和硬件设备。

image.png

由于内核进程在运行时处于内核态,所以它们具有更高的优先级和更快的响应能力,能够处理实时事件和紧急情况。同时,内核进程也起到了协调用户空间进程之间资源分配和访问的作用,确保系统的正常运行。

一、进程的概念

1.1什么是进程?

进程:资源的封装单位;

linux用一个PCB来描述进程,即task_struct, 其包含mm,fs,files,signal…

(1)root目录,是一个进程概念,不是系统概念

apropos chroot
man chroot 2

如下图,将分区/dev/sda5挂载到/mnt/a,调用chroot,改变root目录,当前进程下的文件b.txt即位于当前进程的根目录。

(2)fd也是进程级概念

(base) leon@leon-Laptop:/proc/29171$ ls fd -l

总用量 0

lrwx------ 1 leon leon 64 5月 16 10:26 0 -> /dev/pts/19
lrwx------ 1 leon leon 64 5月 16 10:26 1 -> /dev/pts/19
lrwx------ 1 leon leon 64 5月 16 10:26 2 -> /dev/pts/19

(3)pid,系统全局概念

Linux总的PID是有限的,用完PID

: ( ) { : ∣ : & } ; : :()\{:|:\&\};::(){:∣:&};:

每个用户的PID也是有限的

ulimit -u 最大进程数

ulimit –a

(base) leon@leon-Laptop:/proc/29171$ cat /proc/sys/kernel/pid_max

案例:android2.2漏洞

Android提权漏洞分析——rageagainstthecage:

Setuid(shell):rootshell用户 PID用完时,降权失败,依然具有root权限

解决办法,检查返回值

软件工程符合墨菲定律,解决办法,代码写出闭环。

1.2linux进程的组织方式

linux里的多个进程,其实就是管理多个task_struct,那他们是怎么组织联系的呢?

组织task_struct的数据结构:

  • a.链表,遍历进程
  • b.树:方便查找父子相关进程
  • c.哈希表:用于快速查找

用三种数据结构来管理task_struct,以空间换时间。父进程监控子进程,linux总是白发人送黑发人。父进程通过wait,读取task_struct的退出码,得知进程死亡原因。并且清理子进程尸体。

Android/或者服务器,都会用由父进程监控子进程状态,适时重启等;

1.3进程的生命周期

有六个状态:就绪,运行,睡眠(深度/浅度睡眠),暂停,僵尸

1)什么是僵尸进程:子进程死掉,父进程还没清理尸体,没火化。

子进程死亡后,首先变成僵尸,mm,fs等所有资源已经释放,只剩task_struct躯壳还没被父进程清理,父进程通过wait_pid获得,wait结束,僵尸所有资源(task_struct)被释放。

父进程用waitpid()查看task_struct退出码,检测子进程退出状态;Waitpid()调用完成,则子进程彻底消失。

僵尸进程状态:子进程死亡,还没被父进程清理,资源已经被释放,只剩下task_struct。

清理办法:kill父进程。

僵尸进程被杀死的假象:当一个进程里有多个子线程,主线程退出其他线程仍然运行;

top是以进程视角看线程,所以造成僵尸进程亦然可以被杀的假象;

2)stop状态:其他进程控制其停止

  • Ctrl+z:让进程暂停;发信号19
  • Fg:进程在前台继续跑
  • Bg:让进程在后台继续跑
  • Fg/bg实际上是发continue信号18用于作业控制。
  • Kill –l cpulimit -l 20 -p 3637限制CPU占用率为20,实际用ctrl+z fg/bg实现的。
  • cpulimit是暴力的方法,更好的用CGROUP

3)睡眠

进程本身主动睡眠,等待资源,深度睡眠/浅度睡眠。

调度:只管在就绪态和运行态进程的切换,一个运行态,多个就绪态。调度进程只等CPU,其他资源全部就绪。Linux就绪和占用都是用task_running标识符。

1.4fork

创建进程,子进程拷贝父进程的task_struct资源。

什么是内存泄漏?

进程没死,运行越久,耗费内存越多,叫内存泄漏(程序死亡时,所占内存会全部释放);

判断内存泄漏方法:连续多点采样(6,7,8,9每小时采样,统计剩余内存是否收敛),正常的程序,内存震荡收敛。随时间增加,内存消耗不断增多,且不收敛,则一定是内存泄漏;

发散

二、进程和线程的本质

2.1进程拥有资源mm,fs,files,signal

fork创建一个新进程,也需要创建task_struct所有资源;实际上创建一个新进程之初,子进程完全拷贝父进程资源,如下图示:

比如fs结构体:

子进程会拷贝一份fs_struct,

*p2_fs = *p1_fs;

pwd路径和root路径与父进程相同,子进程修改当前路径,就会修改其p2_fs->pwd值;父进程修改当前路径,修改p1_fs->pwd;

其他资源大体与fs类似,最复杂的是mm拷贝,需借助MMU来完成拷贝;即写时拷贝技术:

2.2写时拷贝技术

#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int data = 10;
int child_process()
{
  printf(“Child process %d, data %d\n”,getpid(),data);
  data = 20;
  printf(“Child process %d, data %d\n”,getpid(),data);
  _exit(0);
}
int main(int argc, char* argv[])
{
  int pid;
  pid = fork();
  if(pid==0) {
    child_process();
  }
  else{
    sleep(1);
    printf(“Parent process %d, data %d\n”,getpid(), data);
    exit(0);
  }
  return 0;
}

第一阶段:只有一个进程P1,数据段可读可写:

第二阶段,调用fork之后创建子进程P2,P2完全拷贝一份P1的mm_struct,其指针指向相同地址,即P1/P2虚拟地址,物理地址完全相同,但该内存的页表地址变为只读;

第三阶段:当P2改写data时,子进程改写只读内存,会引起内存缺页中断,在ISR中申请一片新内存,通常是4K,把P1进程的data拷贝到这4K新内存。再修改页表,改变虚实地址转换关系,使物理地址指向新申请的4K,这样子进程P2就得到新的4K内存,并修改权限为可读写,然后从中断返回到P2进程写data才会成功。整个过程虚拟地址不变,对应用程序员来说,感觉不到地址变化。

谁先写,谁申请新物理内存;Data=20;这句代码经过了赋值无写权限,引起缺页中断,申请内存,修改页表,拷贝数据…回到data=20再次赋值,所以整个执行时间会很长。

这就是linux中的写时拷贝技术(copy on write), 谁先写谁申请新内存,没有优先顺序;cow依赖硬件MMU实现,没有MMU的系统就没法实现cow,也就不支持fork函数,只有vfork;

2.3 vfork的 mm指针直接指向父进程mm

除了mm共享,其他资源全都拷贝一份,而fork是所有资源都对拷一份,对比如下图

不同点:vfork会阻塞:

vfork后,父进程会阻塞,直到子进程调用exit()或exec,否则父进程一直阻塞不执行;

上面代码改用vfork,打印输出10,20,20

2.4线程

clone函数创建一个新进程,不执行任何拷贝,所有资源都等同vfork中的mm共享,task_struct里只有指针指向父进程task_struct;

也就是子进程与父进程完全共享资源,但是又可以被独立调度,实际上这就是linux中的线程本质;

pthread_create()函数就是调用clone()函数(带有clone_flags)创建新task_struct,其内部mm,fs等指针全都指向父进程task_struct;

Linux中创建进程(fork,vfork)和线程(pthread_create),在内核都是调用do_fork()–>clone(),参数clone_flags标记表明哪些资源是需要克隆的,创建线程时,所有资源都克隆;

从调度的角度理解线程,从资源角度来理解进程,内核里只要是task_struct,就可以被调度;linux中的线程又叫轻量级进程lwp;

ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror(“cannot create new thread”);
return -1;
}
strace ./a.out

2.5进程守护

如上述,资源全部共享是线程,不共享是进程;那假如修改clone函数中的clone_flags,使共享其中部分资源,如下图示:

这时候创建的既不是进程也不是线程,妖有了仁慈的心,就不再是妖,是人妖;

Linux是可以调用clone创建人妖的,不过没实际必要~

2.6PID

Linux 的每个线程都会创建task_struct,会有个独立的PID;

POSIX标准规定,在多线程中调用getpid()应该获得相同的PID;

为兼容POSIX标准,linux增加了一层TGID,调用getpid()实际上是去TGID层获取PID,TGID中PID均相同,保留了线程在内核中不同的PID,如下图所示:

top命令看到的是进程TGID,所有线程相同;

top–H命令是从线程视角,此时的PID是task_struct中实际的PID;

2.7进程死亡

子进程先死亡,父进程去清理,所谓白发人送黑发人,不清理则变成僵尸进程;

若父进程先死,子进程变成孤儿,一般托付给init,新版linux3.4引入subreaper,可以托付给中间进程subreaper。

父进程先死亡后,子进程沿tree向上找最近的subreaper挂靠,找不到subreaper,就挂在init。

/* Become reaper of our children */
if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
log_warning(“Failed to make us a subreaper: %m”);
if (errno == EINVAL)
log_info(“Perhaps the kernel version is too old (< 3.4?)”);
}

PR_SET_CHILD_SUBREAPER设置为非零值,当前进程就会变成subreaper,会像1号进程那样收养孤儿进程;

2.8睡眠

当进程需要等待硬件I/O资源的时候,可以设置为睡眠状态,一般驱动做成浅度睡眠,硬盘等资源会置入深度睡眠(不会被信号唤醒);

睡眠是把task_struct挂在wait queue上,比如多个进程都在等待串口,当串口可用时,唤醒等待队列上所有进程;以下为《linux设备驱动开发详解》中案例注释

注:上图有个错误,while循环中,应该为

“若非阻塞,直接退出”

”进程阻塞,将进程设置睡眠状态“

当读取fifo为空即dev->current_len==0时,将进程加入等待队列睡眠,schedule()让出CPU,fifo中写入数据时将等待队列唤醒,此函数中schedule()处继续执行;唤醒动作在write函数中执行;

唤醒后检查唤醒原因,若为IO唤醒,正常读取数据;若为信号唤醒,直接退出

0进程

0进程是唯一没通过fork()创建的进程,是系统中所有其它用户进程的祖先进程,其创建1号进程(init进程后),退化为idle进程,也叫swapper进程;

top命令中的id时间即为idle进程运行时间;idle进程:优先级是最低的,当系统中没有任何进程运行时,即执行idle进程,idle将CPU置入低功耗模式,有任何其他进程被唤醒,idle即让出CPU;

idle进程的设计,实际上是将“跑”与“不跑”的问题,统一为“跑”的问题。极巧妙的简化了系统设计,降低进程之间的耦合度。(将检查系统是否空闲,设置CPU低功耗模式的功能放在idle实现,其他进程都不用关心CPU工作模式)

ARM版本实现如下:

Wfi ==> wait for interrupt

对于用户空间来说,进程的鼻祖是init进程,所有用户空间的进程都由init进程创建和派生,只有init进程才会设置SIGNAL_UNKILLABLE标志位。

如果init进程或者容器init进程要使用CLONE_PARENT创建兄弟进程,那么该进程无法由init回收,父进程idle进程也无能为力,因此会变成僵尸进程(zombie)。

【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!

相关文章
|
5天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
26 6
|
2天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
33 8
|
3天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
2天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
18 6
|
3天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
18 5
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
3天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
4天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
3天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
3天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
17 2