今天我们来快速复习一下 Linux Kernel 相关的知识点。
版本历史
https://en.wikipedia.org/wiki/Linux_kernel_version_history
内核版本规范:3.10.0-693.el7.x86_64
内核版本号通常由几个部分组成,其中的每个部分都具有特定的含义。对于你提供的内核版本号 "3.10.0-693.el7.x86_64",它可以分解为以下部分:
1.主要版本号 (Major Version):3
o这是内核的主要版本号,通常表示内核的基本版本。更高的主要版本号通常意味着重大的更改和升级。
2.次要版本号 (Minor Version):10
o次要版本号表示内核的次要更新或功能增强。通常,次要版本之间的更改较小,但可能包含新功能和改进。
3.发布号 (Patch Number):0
o发布号表示内核的补丁或小更新。通常,这个数字会在解决一些问题或漏洞时递增。
4.修订号 (Revision Number):693
o修订号用于指示内核的具体构建或修订版本。这个数字可能随着不同构建的发布而变化。
5.发行版本 (Release Version):el7
o发行版本通常表示内核是为特定操作系统版本构建的。在这种情况下,"el7" 表示这个内核适用于基于Enterprise Linux 7的发行版。
6.架构 (Architecture):x86_64
o架构表示内核编译的目标硬件架构。在这种情况下,"x86_64" 表示这个内核适用于64位x86架构的处理器。
这个版本号的完整含义是,这是一个主要版本为3,次要版本为10,补丁号为0,修订号为693的内核,适用于基于Enterprise Linux 7的64位x86架构系统。这些信息对于系统管理员和开发人员来说很重要,因为它们帮助确定内核的功能和兼容性。如果需要更新内核或解决特定问题,通常需要查找适合操作系统版本和硬件架构的内核版本。
uname -r
3.10.0-693.el7.x86_64
主要版本号 (Major Version):3
这是内核的主要版本号,通常表示内核的基本版本。更高的主要版本号通常意味着重大的更改和升级。
次要版本号 (Minor Version):10
次要版本号表示内核的次要更新或功能增强。通常,次要版本之间的更改较小,但可能包含新功能和改进。
发布号 (Patch Number):0
发布号表示内核的补丁或小更新。通常,这个数字会在解决一些问题或漏洞时递增。
修订号 (Revision Number):693
修订号用于指示内核的具体构建或修订版本。这个数字可能随着不同构建的发布而变化。
发行版本 (Release Version):el7
发行版本通常表示内核是为特定操作系统版本构建的。在这种情况下,"el7" 表示这个内核适用于基于Enterprise Linux 7的发行版。
架构 (Architecture):x86_64
架构表示内核编译的目标硬件架构。在这种情况下,"x86_64" 表示这个内核适用于64位x86架构的处理器。
内核
内核是操作系统的核心软件。
内核态与用户态
内核是运行在特殊 CPU 模式下的程序,这一特殊的 CPU 模式叫做【内核态】,在这一状态下,设备的一切访问及特权指令的执行都是被允许的。
用户程序(进程)运行在【用户态】下,对于内核特权操作(例如 I/O)的请求是通过系统调用传递的。
内核态和用户态是在处理器上使用特权环(或保护环)实现的。
在传统内核里,系统调用会做上下文切换,从用户态切换到内核态,然后执行系统调用的代码。
双向视角:工作负载 vs 资源
User mode |
User applications |
bash, LibreOffice, GIMP, Blender, 0 A.D., Mozilla Firefox, ... |
|
||||
System components |
init daemon: |
System daemons: |
Window manager: |
Graphics: |
Other libraries: |
||
C standard library |
|
|
|||||
Kernel mode |
Linux kernel |
|
|
||||
Process scheduling subsystem |
IPC subsystem |
Memory management subsystem |
Virtual files subsystem |
Networking subsystem |
|
||
Other components: ALSA, DRI, evdev, klibc, LVM, device mapper, Linux Network Scheduler, Netfilter |
|
||||||
Hardware (CPU, main memory, data storage devices, etc.) |
|
||||||
系统调用
Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。此外,C库提供了POSIX的绝大部分API。
在Linux中,每个系统调用被赋了一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用:进程不会提及系统调用的名称。
关键系统调用
系统调用 |
描述 |
read(2) |
读取字节 |
write(2) |
写入字节 |
open(2) |
打开文件 |
close(2) |
关闭文件 |
fork(2) |
创建新进程 |
clone(2) |
克隆新进程或线程 |
exec(2) |
执行新程序 |
connect(2) |
连接到网络主机 |
accept(2) |
接受网络连接 |
stat(2) |
获取文件统计信息 |
ioctI(2) |
设置 V0 属性,或者做其他事情 |
mmap(2) |
把文件映射到内存地址空间 |
brk(2) |
扩展堆指针 |
futex (2) |
快速用户空间互斥锁 |
中断
CPU 通过时分复用来处理很多任务,这其中包括一些硬件任务,例如磁盘读写、键盘输入,也包括一些软件任务,例如网络包处理。在任意时刻,一个 CPU 只能处理一个任务。当某个硬件或软件任务此刻没有执行,但它希望 CPU 来立即处理时,就会给 CPU 发送一个中断请求 —— 希望 CPU 停下手头的工作,优先服务“我”。中断是以事件的方式通知 CPU 的,因此我们常看到 “XX 条件下会触发 XX 中断事件” 的表述。
两种类型:
1.外部或硬件产生的中断,例如键盘按键。
2.软件产生的中断,异常事件产生的中断,例如除以零 。
硬中断
中断处理流程
中断随时可能发生,发生之后必须马上得到处理。收到中断事件后的处理流程:
1.抢占当前任务:内核必须暂停正在执行的进程;
2.执行中断处理函数(ISR):找到对应的中断处理函数,将 CPU 交给它(执行),ISR 位于 Interrupt Vector table,这个 table 位于内存中的固定地址。
3.中断处理完成之后:第 1 步被抢占的进程恢复执行。在中断处理完成之后,处理器恢复执行被中断的进程(resume the interrupted process)。
中断类型
在内核中,发生异常(exception)之后一般是给被中断的进程发送一个 Unix 信号,以此来唤醒它,这也是为什么内核能如此迅速地处理异常的原因。
但对于外部硬件中断(external hardware interrupts)这种方式是不行的, 外部中断处理取决于中断的类型(type):
1.I/O interrupts(IO 中断);
2.例如 PCI 总线架构,多个设备共享相同的 IRQ line。必须处理非常快。内核典型处理过程:
1.将 IRQ 值和寄存器状态保存到内核栈上(kernel stack);
2.给负责这个 IRQ line 的硬件控制器发送一个确认通知;
3.执行与这个设备相关的中断服务例程(ISR);
4.恢复寄存器状态,从中断中返回。
3.Timer interrupts(定时器中断);
4.Interprocessor interrupts(IPI,进程间中断)
执行足够快 vs 逻辑比较复杂
IRQ handler 的两个特点:
5.执行要非常快,否则会导致事件(和数据)丢失;
6.需要做的事情可能非常多,逻辑很复杂,例如收包
这里就有了内在矛盾。
延后中断处理(deferred interrupt handling)
传统上,解决这个内在矛盾的方式是将中断处理分为两部分:
7.top half
8.bottom half
这种方式称为中断的推迟处理或延后处理。以前这是唯一的推迟方式,但现在不是了。现在已经是个通用术语,泛指各种推迟执行中断处理的方式。按这种方式,中断会分为两部分:
·第一部分:只进行最重要、必须得在硬中断上下文中执行的部分;剩下的处理作为第二部分,放入一个待处理队列;
·第二部分:一般是调度器根据轻重缓急来调度执行,不在硬中断上下文中执行。
Linux 中的三种推迟中断(deferred interrupts):
·softirq
·tasklet
·workqueue
软中断
软中断子系统
软中断是一个内核子系统:
9.每个 CPU 上会初始化一个 ksoftirqd 内核线程,负责处理各种类型的 softirq 中断事件;
10.软中断事件的 handler 提前注册到 softirq 子系统;
11.软中断占 CPU 的总开销:可以用 top 查看,里面 si 字段就是系统的软中断开销(第三行倒数第二个指标);
避免软中断占用过多 CPU
软中断方式的潜在影响:推迟执行部分(比如 softirq)可能会占用较长的时间,在这个时间段内, 用户空间线程只能等待。反映在 top 里面,就是 si 占比。
硬中断 -> 软中断 调用栈
前面提到,softirq 是一种推迟中断处理机制,将 IRQ 的大部分处理逻辑推迟到了这里执行。 两条路径都会执行到 softirq 主处理逻辑 __do_softirq(),
12.CPU 调度到 ksoftirqd 线程时,会执行到 __do_softirq();
13.每次硬中断(IRQ)handler 退出时: do_IRQ() -> ...。
软中断触发执行的步骤
每个软中断会经过下面几个阶段:
14.通过 open_softirq() 注册软中断处理函数;
15.通过 raise_softirq() 将一个软中断 标记为 deferred interrupt,这会唤醒该软中断(但还没有开始处理);
16.内核调度器调度到 ksoftirqd 内核线程时,会将所有等待处理的 deferred interrupt (也就是 softirq)拿出来,执行对应的处理方法(softirq handler);
以收包软中断为例,
·IRQ handler 并不执行 NAPI,只是触发它,在里面会执行到 raise NET_RX_SOFTIRQ;
·真正的执行在 softirq,里面会调用网卡的 poll() 方法收包;
·IRQ handler 中会调用 napi_schedule(),然后启动 NAPI poll()。
这里需要注意,虽然 IRQ handler 做的事情非常少,但是接下来 处理这个包的 softirq 和 IRQ 在同一个 CPU 运行。这就是说,如果大量的包都放到了同一个 RX queue,那虽然 IRQ 的开销可能并不多, 但这个 CPU 仍然会非常繁忙,都花在 softirq 上了。
三种推迟执行方式(softirq/tasklet/workqueue)
前面提到,Linux 中的三种推迟中断执行的方式:
·softirq
·tasklet
·workqueue
其中,
17.softirq 和 tasklet 依赖软中断子系统,运行在软中断上下文中;
18.workqueue 不依赖软中断子系统,运行在进程上下文中。
softirq:静态机制,内核编译时确定
Linux 在每个 CPU 上会创建一个 ksoftirqd内核线程。softirqs 是在 Linux 内核编译时就确定好的,例如网络收包对应的 NET_RX_SOFTIRQ 软中断。因此是一种静态机制。如果想加一种新 softirq 类型,就需要修改并重新编译内核。
tasklet:动态机制,基于 softirq
如果对内核源码有一定了解就会发现,softirq 用到的地方非常少,原因之一就是上面提到的,它是静态编译的, 靠内置的 ksoftirqd 线程来调度内置的那 9 种 softirq。如果想新加一种,就得修改并重新编译内核, 所以开发成本非常高。实际上,实现推迟执行的更常用方式 tasklet。它构建在 softirq 机制之上, 具体来说就是使用了上面提到的两种 softirq:
·HI_SOFTIRQ
·TASKLET_SOFTIRQ
换句话说,tasklet 是可以在运行时(runtime)创建和初始化的 softirq。
workqueue:动态机制,运行在进程上下文
这也是一种推迟执行机制,与 tasklet 有点类似,但也有很大不同。
·tasklet 是运行在 softirq 上下文中;
·workqueue 运行在内核进程上下文中;这意味着 wq 不能像 tasklet 那样是原子的;
·tasklet 永远运行在指定 CPU,这是初始化时就确定了的;
·workqueue 默认行为也是这样,但是可以通过配置修改这种行为。
idle process 与中断
为什么需要 idle process
idle process 用于 process accouting,以及降低能耗。在设计上,调度器没有进程可调度时(例如所有进程都在等待输入),需要停下来,什么都不做,等待下一个中断把它唤醒。中断可能来自外设(例如网络包、磁盘读操作完成),也可能来自某个进程的定时器。
Linux 调度器中,实现这种“什么都不做”的方式就是引入了 idle 进程。
·只有当没有任何其他进程需要调度时,才会调度到 idle 进程(因此它的优先级是最低的)。
·在实现上,这个 idle 进程其实就是内核自身的一部分。
·当执行到 idle 进程时,它的行为就是“等待中断事件”。
Linux 会为每个 CPU 创建一个 idle task(因为每个 CPU 上一个调度器),并固定在这个 CPU 上执行。当这个 CPU 上没有其他进程可执行时,就会调度到 idle 进程。它的开销就是 top 里面的 id 统计。
注意,这个 idle process 和 process 的 idle 状态是两个完全不相关的东西,后者指的是 process 在等待某个事件(例如 I/O 事件)。
top - 12:54:29 up 126 days, 1:13, 1 user, load average: 34.04, 29.99, 28.78
Tasks: 966 total, 1 running, 965 sleeping, 0 stopped, 0 zombie
%Cpu(s): 54.1 us, 3.5 sy, 0.0 ni, 39.7 id, 2.1 wa, 0.0 hi, 0.6 si, 0.0 st
KiB Mem : 26386033+total, 1401048 free, 25674694+used, 5712344 buff/cache
KiB Swap: 4194300 total, 2496 free, 4191804 used. 841164 avail Mem
cat /proc/interrupts
cat /proc/softirqs
进程
进程是用以执行用户级别程序的环境,包括内存地址空间、文件描述符、线程栈、寄存器。
进程创建与销毁
进程 fork
进程 exec
进程状态
进程的状态如下:
·D:uninterruptible sleep (usually IO)
·R:running or runnable (on run queue)
·S:interruptible sleep (waiting for an event to complete)
·T:stopped by job control signal
·t:stopped by debugger during the tracing
·W:paging (not valid since the 2.6.xx kernel)
·X:dead (should never be seen)
·Z:defunct ("zombie") process, terminated but not - reaped by its parent
1、R (TASK_RUNNING),可执行状态
只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进而,进程调度器就从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。
很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在 linux下统一为 TASK_RUNNING状态。
2、S (TASK_INTERRUPTIBLE),可中断的睡眠状态
处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构(进程控制块)被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
通过 ps 命令会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么几个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。
3、D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态
与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态。
而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。例如,在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。
4、T/t (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态
T (TASK_STOPPED)状态:向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。SIGSTOP与SIGKILL信号一样,是非常强制的。不允许用户进程通过signal系列的系统调用重新设置对应的信号处理函数。向进程发送一个SIGCONT信号(kill -18),可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态;或者kill -9直接尝试杀死。
t (TASK_STOPPED)状态:当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在gdb(UNIX及UNIX-like下的调试工具)调试中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。而在其他时候,被跟踪的进程还是处于前面提到的那些状态。
对于进程本身来说,TASK_STOPPED和TASK_TRACED状态很类似,都是表示进程暂停下来。而TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。
5、Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程
进程在退出的过程中,处于TASK_DEAD状态。在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息(保存在task_struct里)。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。
当父/子进程在不同时间点退出时,就可能会出现Z的细分状态:
19.僵尸状态 一个进程使用 fork 创建子进程,如果子进程退出后父进程没有调用 wait 或 waitpid 获取子进程的状态信息,并将子进程释放掉。那么子进程的进程描述符仍然保存在系统中,仍然占用进程表,此时进程就处于僵尸状态。子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。出现僵尸状态可能有两种情况:第一种情况,父进程收到通知还没来得及完成收尸,此时正常;第二种情况,父进程收尸出现异常,此时,只要父进程不退出,子进程的僵尸状态就一直存在,可以通过杀死父进程或者重启来解决。
20.孤儿状态 父进程退出,相应的一个或多个子进程还在运行,那么那些子进程将处于孤儿状态,成为孤儿进程。这些进程会被托管给别的进程,托管给谁呢?可能是退出进程所在进程组的下一个进程(如果存在的话),或者是1号进程。所以每个进程、每时每刻都有父进程存在。除非它是1号进程。1号进程,pid为1的进程,又称init进程。linux系统启动后,第一个被创建的用户态进程就是init进程。它有两项使命:1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作;init进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态,“收尸”过程中则处于TASK_RUNNING状态。
6、X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁
进程在退出过程中也可能不会保留它的task_struct。比如这个进程是多线程程序中被detach过的进程。或者父进程通过设置SIGCHLD信号的handler为SIG_IGN,显式的忽略了SIGCHLD信号。(这是posix的规定,尽管子进程的退出信号可以被设置为SIGCHLD以外的其他信号。) 此时,进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码立即就会将该进程彻底释放。所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。
ps -lf -p 1
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
4 S root 1 0 6 80 0 - 49338 ep_pol 5月08 ? 8-05:37:56 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
F: 进程标志符,表示进程的状态,其中4表示进程在内核中运行。
S: 进程状态,其中S表示睡眠状态(Sleeping)。
UID: 进程的用户标识符,这里是 root 用户。
PID: 进程号,这里是1。
PPID: 父进程号,这里是0,表示没有父进程。
C: 进程的 CPU 使用百分比。
PRI: 进程的调度优先级。
NI: 进程的 nice 值,这是进程的优先级。
ADDR: 进程的地址空间标识符。
SZ: 进程的虚拟内存大小。
WCHAN: 进程正在等待的内核函数。
STIME: 进程的启动时间。
TTY: 进程所属的终端。
TIME: 进程的累计 CPU 时间,格式为:days-HH:MM:SS
CMD: 进程的命令行。
进程小状态:
·<:high-priority (not nice to other users)
·N:low-priority (nice to other users)
·L:has pages locked into memory (for real-time and custom IO)
·s:is a session leader
·l:is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
·+:is in the foreground process group
常用的命令:ps命令
·ps -eo stat,pid,user,%cpu,%mem,time,cmd
·ps -eo stat,pid,user,%cpu,%mem,time,cmd | grep -e '^[R]'
进程描述符
标示符:描述本进程的唯一标示符,用来区别其他进程。
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和正在被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等。
+-------------------------------------------------------------------+
| dr-x------ 2 tavi tavi 0 2021 03 14 12:34 . |
| dr-xr-xr-x 6 tavi tavi 0 2021 03 14 12:34 .. |
| lrwx------ 1 tavi tavi 64 2021 03 14 12:34 0 -> /dev/pts/4 |
+--->| lrwx------ 1 tavi tavi 64 2021 03 14 12:34 1 -> /dev/pts/4 |
| | lrwx------ 1 tavi tavi 64 2021 03 14 12:34 2 -> /dev/pts/4 |
| | lr-x------ 1 tavi tavi 64 2021 03 14 12:34 3 -> /proc/18312/fd |
| +-------------------------------------------------------------------+
| +----------------------------------------------------------------+
| | 08048000-0804c000 r-xp 00000000 08:02 16875609 /bin/cat |
$ ls -1 /proc/self/ | 0804c000-0804d000 rw-p 00003000 08:02 16875609 /bin/cat |
cmdline | | 0804d000-0806e000 rw-p 0804d000 00:00 0 [heap] |
cwd | | ... |
environ | +----------->| b7f46000-b7f49000 rw-p b7f46000 00:00 0 |
exe | | | b7f59000-b7f5b000 rw-p b7f59000 00:00 0 |
fd --------+ | | b7f5b000-b7f77000 r-xp 00000000 08:02 11601524 /lib/ld-2.7.so |
fdinfo | | b7f77000-b7f79000 rw-p 0001b000 08:02 11601524 /lib/ld-2.7.so |
maps -----------+ | bfa05000-bfa1a000 rw-p bffeb000 00:00 0 [stack] |
mem | ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso] |
root +----------------------------------------------------------------+
stat +----------------------------+
statm | Name: cat |
status ------+ | State: R (running) |
task | | Tgid: 18205 |
wchan +------>| Pid: 18205 |
| PPid: 18133 |
| Uid: 1000 1000 1000 1000 |
| Gid: 1000 1000 1000 1000 |
+----------------------------+
进程状态转换
Linux 三个特殊进程
Linux下有3个特殊的进程:
·idle 进程 (pid = 0)
·init 进程 (pid = 1) — Redhat/CentOS 中就是 systemd 进程
·kthreadd (pid = 2)
idle 进程,pid = 0
·由系统自动创建, 运行在内核态;
·pid = 0,其前身是系统创建的第一个进程
·唯一一个没有通过 fork 或者 kernel_thread 产生的进程;
·完成加载系统后,演变为进程调度、交换;
init 进程,pid = 1
·init 进程由 idle 通过 kernel_thread 创建,在内核空间完成初始化后, 加载 init 程序, 并最终用户空间
·由 0 进程创建,完成系统的初始化。是系统中所有其它用户进程的祖先进程
·Linux 中的所有进程都是有 init 进程创建并运行的 - 首先Linux内核启动,然后在用户空间中启动 init 进程,再启动其他系统进程。在系统启动完成完成后,init 将变为守护进程监视系统其他进程。
kthreadd 进程,pid = 2
·kthreadd 进程由 idle 通过 kernel_thread 创建,并始终运行在内核空间, 负责所有内核线程的调度和管理
·它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthread 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程
ps -f -p 2
UID PID PPID C STIME TTY TIME CMD
root 2 0 0 4月01 ? 00:00:47 [kthreadd]
参考
https://www.kernel.org/
https://en.wikipedia.org/wiki/Linux_kernel
https://github.com/linuxkerneltravel/linux_kernel_wiki
https://kcjia.cn/2022/07/27/linux-syscall-table/
http://arthurchiao.art/blog/linux-irq-softirq-zh/
https://cloud.tencent.com/developer/article/1568077
https://tldp.org/LDP/tlk/kernel/processes.html
https://linux-kernel-labs.github.io/refs/heads/master/so2/index.html