Linux操作系统 —— 进程(四)
本系列,是自己学习Linux过程中的笔记。
希望读者在看完全文后,也能留下你们的经验或者问题。
如果能从这里学到点东西,记得请我喝杯☕☕☕~
—— MinRam
一、前言 Overview
进程,就是运行的程序。
进程 Process, 是操作系统提供给用户程序的一种抽象概念。内核篇讲过,程序本身是静态的一串代码,且存放在磁盘。在需要的时候由操作系统,将程序从磁盘中拿出,进行一系列操作后,程序开始运行,也就是进程。
而在操作系统中,往往会同时运行多个程序,浏览器、IDE、聊天工具等等,在现代操作系统中,往往会有几十个程序同时在跑。每个运行中的程序都需要跟CPU进行交互处理各式各样的计算任务。而CPU往往只有几个有限的核心。
面对这种僧多粥少的问题,操作系统给用户程序提供了CPU虚拟化技术,其目的就是产生无限多的CPU的假象,让每个用户程序觉得自己独占一个CPU核心。这一实现方式就是采用时分共享
(time sharing)实现的。这一技术的基础调度单位就是进程。
可以看出,进程就是系统对CPU进行抽象,提供给应用程序使用。用户应用程序只跟进程打交道,无法触碰到实际CPU。而操作系统中进程管理子程序,也只关注进程这个抽象,再将它分配给实际CPU核心。这样用户程序并不需要关心当前有没有CPU可以用,而操作系统也不需要关心用户程序需不需要使用CPU。就这样操作系统实现了多个用户应用程序之间复用同一个或者多个CPU核心。
时分共享的机制,也运用在其他类型的资源上。
二、进程 Process
进程,程序的实例
2.1 进程的组成
对于操作系统来说,运行中的程序就是进程。我们可以通过了解进程运行过程中的一系列行为和操作,来了解进程。
那么需要了解对于执行进程来说,什么是需要我们关注的。所以进程由以下几个部分组成:
- 进程标识 Process ID. 进程的标识符,除了本身的PID以外,还应当记录父进程的Parent PID。
- 进程状态 State 进程状态.
- 虚拟内存地址空间 Address Space. 包括主存(用户空间、内核空间)、寄存器(一般寄存器、控制寄存器)、外部设备(文件、socket、其他内存映射IO设备)等. 程序的数据和变量都是存储在内存上,而程序本身的指令也是存在内存里的。也就是说整个进程都是位于内存上的。所以这一段内存的访问地址,就是进程的一部分。
- 寄存器 Register. 特指特别功能范畴的寄存器,如程序计数器(Program Counter),也被叫做指令指针(Instruction Pointer),属于CPU寄存器,指向下一条指令所在的内存地址。还有栈指针(Stack Pointer) 和函数栈指针(Frame Pointer),则是用来指明函数入口参数,本地变量,以及范围值地址。
- 外接设备信息 I/O infromation. 程序在运行中,可能会读取或者写入一些文件(如磁盘,U盘中的文件),并对其进行独占等操作,所以进程中还需有这些文件列表信息。
2.2 进程的生命周期
对于操作系统,它提供了一些进程的操作接口API,也就对应了进程的生命周期:
- 创建 Create. 进程可以通过多个途径被创建出来,如终端,双击快捷键。系统就会调用起一个进程。/
- 销毁 Destroy. 有生就有死,操作系统必须有途径能强制性地销毁一条进程。正常情况下,在进程代码执行完后,往往会自行推出(exit)。但考虑到异常情况(如死循环,死锁)下,操作系统也能为用户提供销毁的接口(如linux的kill指令)
- 等待 Wait. 有时候进程需要等待另一个进程的结束,也就是阻塞进程。
- 其余操作 Miscellaneous Control. 除了销毁等待,操作系统还提供了其他的操作接口比如挂起(Suspend)和对应的恢复(Resume)
- 状态 Status. 操作系统会提供一些接口,用来查询进程的运行状态,运行时常,PID等信息。
linux下对进程的API有以下几种(后续会详细介绍):
2.3 进程的状态
随着操作系统发展,进程状态也在不断细分,但最基础的也是以下几种:
创建状态 Creating. 前面讲到,在进程创建时,需要申请一块空白的进程控制块(PCB Process Controll Block)。并在向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态。进程的生命周期开始
就绪状态 Ready. 在进程拿到所需资源后,就进入就绪态。这里包括创建准备完成,还有等待条件完成的进程。当进程处于就绪状态时,意味着进程随时可以被CPU激活进入执行状态。但由于执行策略的结果,没有被选中进入执行状态。
执行状态 Running. 进程正在被执行,根据进程的指令执行相应的操作。
阻塞状态 Blocked. 正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用。例如,当进程写入或者读取文件时,会等待I/O请求,这时候就会进入阻塞状态,而处理器就会去执行其他进程。
终止状态 Dead. 进程结束,或出现错误,或被系统终止,进入终止状态。进程的生命周期结束
所以可以看出,进程的核心状态只有三种: 就绪Ready、执行Running、阻塞Blocked。但为用户观察需要,进程还有挂起和激活两种操作。挂起后进程处于静止状态进程不再被系统调用,相反操作则是激活操作。这里不再补充
三、 源码剖析 Code
以Linux内核v5.13.16为例,且依赖库为X86(随便找的版本)。本节需要有一定的代码语法知识,特别是C语言。
操作系统,本身就是一个程序。同样有一些关键的数据结构,用来存储进程状态信息。其内容与上面谈到的逻辑大致一致,但也有具体的实现改动。以下结合代码,比较实际应用中的区别。
首先需要解释下Linux中,进程和线程的区别。在Linux操作系统层面,线程就是特殊的进程,在于跟其他同进程下的线程共享内存空间(代码段,数据段,但不共享栈)
。在用户层面来说,进程和线程是两种不同的概念。而在内核层面中,线程其实也是进程。所以对于Linux内核来说,只有任务TASK这个概念。后续会单独出一章博客完整说明下Task
定义。
回归开头,在Linux中,每个线程都是一个Task
,也都是通过task_struct
结构体来描述的,我们称它为进程描述符。内核进程相关源码链接(source/include/linux/sched.h)。它包含了如下几个内容:
- 标识符 : 唯⼀标⽰符,⽤来区别其他
Task
(线程)。 - 状态 :
Task
当前状态。 - 优先级 : 用于调度用的优先级。
- 程序计数器: Program Counter程序中即将被执⾏的下⼀条指令的地址。
- 内存指针: 包括程序代码和线程相关数据的指针,还有和其他线程共享的内存块的指针
- 上下文数据: 线程执⾏时处理器的寄存器中的数据。
- I/O状态信息:包括显⽰的I/O请求,分配给线程的I/O设备和被线程使⽤的⽂件列表。
- 运行信息: 包括处理器时间总和,使⽤的时钟数总和,时间限制等。
3.1 进程标识符 Process ID
- 截取片段代码如下:
1 | struct task_struct { |
前面讲到,对于Linux系统内核来说,只有线程和线程组。同一线程组内的线程共享内存空间(代码段,数据段,但不共享栈)。所以线程组就是进程的概念。
在task_struct
中,pid
(Process ID)就是指线程的唯一标识,而tgid
(Task Group ID)就是线程组的唯一标识。
- 当创建一个新进程时,会先创建进程的主线程,主线程获取自己的PID,同时主线程的PID就是这个线程组(进程)的唯一标识TGID。
- 当一个线程启动一个新线程时,新线程会获取自己的PID,同时从原始线程继承TGID
当我们用
ps
命令或者getpid()
等接口查询进程ID时,内核返回给我们的也正是这个tgid
。注意不是pid
pid
和 tgid
都是 pid_t
类型,该类型是由各体系type.h
分别定义的__kernel_pid_t
,通常是一个 int
类型,参考include/uapi/asm-generic/posix_types.h。
- 截取部分代码如下:
1
2
3
4
5
6
7// tags/v5.13.16 - include/linux/types.h
typedef __kernel_pid_t pid_t
// tags/v5.13.16 - include/uapi/asm-generic/posix_types.h
typedef int __kernel_pid_t;
在threads.h可以看到关于PID
默认情况下最大值的定义。
1 | /* |
3.2 内核进程状态 Process Status
- 截取状态成员如下:
1
2
3
4
5
6struct task_struct {
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state;
int exit_state;
}
Task
的主要状态由task->state
确定,当进程退出后,会再task->exit_state
同步更新。
- 截取状态定义代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/*
* Task state bitmask. NOTE! These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
/* Used in tsk->state: */
/* Used in tsk->exit_state: */
/* Used in tsk->state again: */
/* Convenience macros for the sake of set_current_state: */
/* Convenience macros for the sake of wake_up(): */
/* get_task_state(): */
可以看到基础进程状态的标识码都是采用
long
类型的,且都是2的幂。这是状态的标准处理方式。我们可以通过逻辑运算-或(OR |)来创建更多的组合状态,也可以用逻辑运算-与(AND &)来验证。所以对于通用32位系统,我们最多只有32种基础状态。
例如:#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
Linux中进程状态的基础状态(TASK_RUNNING
,TASK_INTERRUPTIBLE
,TASK_UNINTERRUPTIBLE
,__TASK_STOPPED
,__TASK_TRACED
,EXIT_DEAD
,EXIT_ZOMBIE
),又分为两类: state(运行中的状态) 和 exit_state(退出状态)。
TASK_RUNNING
表示进程处于执行状态或者就绪状态。进程处于TASK_RUNNING时,并不意味着进程处于执行状态。只有被current指向的进程才是执行中的进程。
系统中有一个运行队列(Run_Queue),存放处于TASK_RUNNING状态的进程。在系统执行调度进程时,会从该运行队列中选择符合条件的进程。current永远指向运行队列中的某个进程。
TASK_INTERRUPTIBLE
表示进程处于阻塞状态,在等待某个资源或某个事件(比如等待Socket连接、等待信号量)。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒,转成TASK_RUNNING。
通过
ps
的指令我们可以看到进程列表的大部分的进程都是处于该状态下。在阻塞状态的进程,都位于系统的等待队列(Wait_Queue)中。TASK_UNINTERRUPTIBLE
表示进程处于阻塞状态。不同于TASK_INTERRUPTIBLE的是,该状态下的进程是不可中断的。
不可中断,指的是进程不响应异步信号,指的并不是CPU不响应外部硬件的中断。
Linux中阻塞状态的进程分为两种:可中断(TASK_INTERRUPTIBLE)和不可中断(TASK_UNINTERRUPTIBLE)的阻塞状态。
- TASK_INTERRUPTIBLE: 如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度。也可以接受
kill
指令异步给的信号。 - TASK_UNINTERRUPTIBLE: 往往因为硬件资源不能满足而阻塞,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数
wake_up()
等。
而TASK_UNINTERRUPTIBLE状态存在的意义就在于,
保护内核的某些处理流程是不被打断
。如果进程响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过
ps
命令基本上不可能捕捉到。应注意,部分工程师喜欢将进程设置为TASK_UNINTERRUPTIBLE,将导致一系列问题。当唤醒条件无法满足时候,进程是无法接受
的异步指令的。最终只能通过重启系统解决。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。所以Linux Kernel在2.6.25引进了新状态 TASK_KILLABLE
,解决这一问题。- TASK_INTERRUPTIBLE: 如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度。也可以接受
TASK_KILLABLE
TASK_KILLABLE = TASK_UNINTERRUPTIBLE + TASK_WAKEKILL
表示进程处于阻塞状态。跟TASK_UNINTERRUPTIBLE类似,该下的进程是不可中断的。但又为了弥补TASK_UNINTERRUPTIBLE的缺陷(唤醒条件无法满足)。该状态下的进程只接受终止信号的异步信息,其余一概不接受。
__TASK_STOPPED
表示进程被暂停了。通常当进程(TASK_UNINTERRUPTIBLE状态下的进程除外)接收到
SIGSTOP
、SIGTSTP
、SIGTTIN
或SIGTTOU
信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。当进程接收到SIGCONT
后,则恢复到TASK_RUNNING状态。对于该状态的进程,仍处于系统的运行队列中。但调度器并不会去执行该进程。
__TASK_TRACED
表示进程被暂停了,且被Debugger程序监听跟踪。
正在被跟踪,指进程暂停,等待调试进程对它进行其他操作。
比如gdb中对跟踪进程标记断点后,进程运行到断点处代码,就会处于TASK_TRACED状态。TASK_TRACED和TASK_STOPPED都是表示进程被暂停。TASK_TRACED中的进程无法被
SIGCONT
信号唤醒。只能等到调试进程通过Ptrace系统调用执行PTRACE_CONT
、PTRACE_DETACH
等操作(通过Ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。也就是TASK_TRACED其实是对调试进程的保护,避免被错误信号恢复执行状态。EXIT_ZOMBIE
表示进程已经终止了。但还未被父进程通过
wait()
调用获知该进程的终止信息,又被称为Zombie的数据结构。该状态的进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。在进程退出后,会进入该状态,就是EXIT_ZOMBIE的进程在等待父进程采集终止信息中。
该状态的进程不可通过kill
清理。如果要清理该进程,要么等到采集时间超时,或者清除父进程。由于该进程还保留PID
,过多的僵尸进程,会影响系统的调度效率。EXIT_DEAD
表示进程的最终状态。
状态转移图:
3.3 内核进程信息 Address of Process
在Linux中,线程有三种重要的数据结构,分别是内核栈、线程描述符和线程联合体
。三者的关系,可以参考下图:
task_struct
,是内核下进程描述符,包含了具体进程的所有信息。
源码可以参考文件/include/linux/sched.h,截取部分代码如下。其中包含了thread_info
和stack
。还将主体成员包含在randomized_struct_fields
中,具体原因可以参考3.4节。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24struct task_struct {
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
/*
* This begins the randomizable portion of task_struct. Only
* scheduling-critical items should be added above here.
*/
randomized_struct_fields_start
void *stack;
...
/*
* New fields for task_struct should be added above here, so that
* they are included in the randomized portion of task_struct.
*/
randomized_struct_fields_end
}thread_info
,是每个架构对进程(内核视角下)的具体实现信息。内核需要存储每个进程的控制块PCB信息,同时Linux要兼容不同的架构(x86,ARM等),每个架构有自己独特的进程信息结构。所以需要有一个方式,将架构相关的内容与Linux进程管理通用部分解耦分离。
用一种解耦的方式来描述进程, 就是
task_struct
, 而thread_info
类型就保存了各架构自己需要的信息,同时我们还在thread_info
中嵌入指向task_struct
的指针, 则我们可以很方便的通过thread_info
来查找task_struct
。task_struct
结构里面通过CONFIG_THREAD_INFO_IN_TASK
宏来控制是否有thread_info
成员结构。thread_info
结构体的内容和具体的体系架构有关,保存了为实现进入、退出内核态的特定架构的汇编代码段所需要访问的部分进程的数据,所以大多数架构采用了独立寄存器或栈寄存器来保存thread_info
地址。下面是ARM体系下的
thread_info
结构,详见源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/*
* low level task data that entry.S needs immediate access to.
* __switch_to() assumes cpu_context follows immediately after cpu_domain.
*/
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
unsigned long stack_canary;
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
struct crunch_state crunchstate;
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
unsigned long thumbee_state; /* ThumbEE Handler Base register */
};在
thread_info
结构中,嵌入了指向task_struct
的成员task
,方便我们通过该结构获取task_struct
。current
相关对于所有Linux兼容的架构,都必须实现
current
和current_thread_info
两个宏定义。current_thread_info
可以获取当前执行进程的thread_info
实例指针current
给出当前进程进程描述符task_struct
的地址,通常由current_thread_info
确定
task_struct
中的stack
是个void类型的指针,指向进程(内核视角)对应的thread_union
thread_union
是Linux将内核栈和线程描述信息thread_info
组合成一个union。详见源代码(source/include/linux/sched.h)。
1
2
3
4
5
6
7
8
9union thread_union {
struct task_struct task;
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};内核定义一个联合体
thread_union
用于将内核栈和thread_info
存储在THREAD_SIZE
大小的空间里,如果没有启用 CONFIG_ARCH_TASK_STRUCT_ON_STACK,那么联合体还会包含了一个task_struct
结构。THREAD_SIZE
指thread_union
的空间大小。
源码详见arch/(架构名称)/include/asm/thread_info.h, 该值与架构类型有关。例如,arm(arch/arm/include/asm/thread_info.h)中的定义如下:
1
2
3
4
5
6
7
8
9
10
/*
* KASan uses a lot of extra stack space so the thread size order needs to
* be increased.
*/从上面的定义可知,如果
PAGE_SIZE
的大小是4K的话,那么THREAD_SIZE
就是8K。thread_union
中的stack
指进程的内核栈进程在内核态运行时需要自己的堆栈信息(不是原用户空间中的栈), 因此Linux内核为每个线程都提供了一个内核栈
kernel stack
,用户态进程所用的栈,是在进程线性地址空间中;而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,并切换堆栈,那么在内核空间中使用的就是这个内核栈。
注意
task_struct
中的stack
并非指向内核栈,而是指向thread_union
的地址。thread_info
和内核栈虽然共用了thread_union
结构, 但是thread_info
大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info
的存储空间时, 则会发生栈溢出。sp
栈寄存器,用来存储当前栈顶地址。以x86源码为例,arch/x86/include/asm/asm.h
1
2
3
4
5
6
7/*
* This output constraint should be used for any inline asm which has a "call"
* instruction. Otherwise the asm may be inserted before the frame pointer
* gets set up by the containing function. If you forget to do this, objtool
* may print a "call without frame pointer save/setup" warning.
*/
register unsigned long current_stack_pointer asm(_ASM_SP);sp
寄存器是CPU栈指针,用来存放栈顶单元的地址。在 x86 系统中,堆栈从顶部开始,并朝着该内存区域的开头增长。从用户态切换到内核态后,进程的内核栈总是空的。因此,sp
寄存器指向这个堆栈的顶部。一旦数据写入堆栈,sp
的值就会递减。通过
sp
,可以拿到thread_info的地址,即在 current
点讲到的current_thread_info
。1
2
3
4
5
6
7
8
9
10static inline struct thread_info *current_thread_info(void) __attribute_const__;
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}THREAD_SIZE
为 8K,其二进制表示为0000 0000 0000 0000 0010 0000 0000 0000
。~(THREAD_SIZE-1)
的结果正好是1111 1111 1111 1111 1110 0000 0000 0000
。
低十三位全为零,也就是sp
的低十三位刚好被屏蔽,最终得到thread_info
的地址。当内核态线程使用了更多的栈空间时,内核栈会溢出到
thread_info
部分,因此内核提供了kstack_end
函数来判断是否在正确的内核栈空间里。参照源码/include/linux/sched/task_stack.h
1
2
3
4
5
6
7
8
9
static inline int kstack_end(void *addr)
{
/* Reliable end of stack detection:
* Some APM bios versions misalign the stack
*/
return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*)));
}
3.4 内核进程其他 Others
记录一些源码中的亮点:
randomized_struct_fields_start
&randomized_struct_fields_end
Linux内核在 4.13 中引入
Structure Randomization
技术来随机化 task_struct 的大部分结构成员布局,用来防止利用结构体偏移来覆盖特定敏感字段(如函数指针)的内核攻击,有兴趣的可以看下这篇文章 Randomizing structure layout。一个
struct
的内部数据存储是按照声明顺序的,因此黑客程序可以很容易得计算出一个关键值,比如跳转函数地址在内核一些结构体中的偏移。修改该偏移的值以后,就可以轻松控制内核执行自己的代码(内核态)。举例代码:
1
2
3
4
5
6struct critical_struct {
int id;
void (* fn) (void *data);
void *data;
...
};有这么一段内核源码,如果入侵程序获得了对应数据段的写权限,那么他可以通过
$STRUCT_OFFSET + 0x04
,也就是fn
的函数地址修改为自己的代码地址,进而获得内核态的执行权限。如果混淆以后,就为这种侵入过程制造了难度。
3.5 进程相关指令 Process Commands
补充下进程相关的简单指令
ps
- 描述: ps命令来自于英文词组”process status“的缩写,其功能是用于显示当前系统的进程状态。使用
ps
命令可以查看到进程的所有信息,例如进程的号码、发起者、系统资源使用占比(处理器与内存)、运行状态等等。帮助我们及时的发现哪些进程出现僵死或不可中断等异常情况。 - 语法格式:
ps [参数]
ps -eLf
可以看到线程信息,其中PID
指进程ID,LWP
指线程ID(task_struct
中的pid
)。
- 描述: ps命令来自于英文词组”process status“的缩写,其功能是用于显示当前系统的进程状态。使用
top
- 描述: Linux中常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,常用于服务端性能分析。
- 语法格式:
top [参数]
默认top,上会显示对应的进程数
top -H
(也可以在进入界面后,再输入大写H),可以看到线程数。
htop
- 描述:
htop
是Linux系统中的一个互动的进程查看器,一个文本模式的应用程序(在控制台或者X终端中),需要ncurses。htop
比较人性化。它可让用户交互式操作,支持颜色主题,可横向或纵向滚动浏览进程列表,并支持鼠标操作。 - 语法格式:
htop [参数]
要在
htop
中启用线程查看,开启htop,然后按F2
来进入htop
的设置菜单。选择Setup
栏下面的Display options
,然后勾选Tree view
和Show custom thread names
选项。按F10
退出设置。- 描述:
推荐几个不错的文档网站:
- Linux命令大全(手册). 中文文档
- Explain Shell . 英文文档,可能需要梯子。
四、总结 Summary
所以进程大致内容就是这些,也是操作系统最基础的抽象概念。简单来说就是运行的程序。接下来会继续阐述进程的其他机制,如系统调用和调度算法。从而了解操作系统是如何实现CPU虚拟化的。
如内容有错误,或不足,请留言或者直接联系我。万分感谢指正。