进程(Processes)
本节内容包括进程与线程的基本概念、上下文切换、阻塞与唤醒机制、进程上下文等。
🔖 进程与线程(Processes and Threads)
进程(process)是操作系统中用于组织与管理资源的抽象概念。每个进程都会拥有以下多个资源:
- 独立的地址空间(address space)
- 一个或多个线程(threads)
- 打开的文件(opened files)
- 网络套接字(sockets)
- 信号量(semaphores)
- 共享内存区(shared memory regions)
- 定时器(timers)
- 信号处理函数(signal handlers)
- 其他多种资源和状态信息
以上所有信息都被组织在进程控制块(Process Control Block, PCB)中。在 Linux 内核里,这个结构体叫作struct\ task_struct
。
📌 进程资源的概览(Overview of process resources)
在 Linux 中,可以通过访问/proc/<pid>
目录来查看某个进程的资源概况,其中<pid>
是进程的ID。例如,/proc/18205
文件夹中包含以下重要内容:
fd/
目录:该进程打开的文件描述符列表,如:0
、1
、2
分别指向标准输入、标准输出、标准错误(通常是终端设备,如/dev/pts/4
)
maps
文件:包含了进程地址空间的内存映射信息,如:- 代码段(如
/bin/cat
); - 堆区(heap);
- 栈区(stack);
- 动态链接库(如
/lib/ld-2.7.so
); - 虚拟动态共享对象(vdso)。
- 代码段(如
status
文件:包含进程状态信息,例如:Name
:进程名;State
:进程状态(如R
表示正在运行);Pid
:当前进程ID;PPid
:父进程ID;Uid
、Gid
:用户ID和用户组ID。
- 进程控制块(PCB,Linux中为
$struct\ task_struct$
):内核用于存储进程状态信息的关键结构体,包括寄存器状态、内存管理、文件描述符、调度信息等。 - 线程(thread):是进程内部的执行单元,同一个进程内多个线程共享地址空间和进程资源,但拥有独立的栈和寄存器上下文。
- 上下文切换(context switching):CPU从一个进程(或线程)切换到另一个时,保存当前执行状态(上下文),并恢复另一个进程的上下文。
- 阻塞与唤醒(blocking and waking up):进程等待资源时会进入阻塞状态(挂起),直到所需资源可用后由内核唤醒。
- 进程上下文(process context):进程执行系统调用进入内核后的状态称为进程上下文,它允许内核安全访问用户空间数据。
结构体 task_struct
让我们仔细看看 struct\ task_struct
结构体。虽然我们可以直接查看源代码,但这里我们将使用一个工具叫做 pahole
(属于 dwarves
安装包的一部分)来深入了解这个结构体:
$ pahole -C task_struct vmlinux
输出如下:
struct task_struct {
struct thread_info thread_info; /* 0 8 */
volatile long int state; /* 8 4 */
void * stack; /* 12 4 */
...
/* --- cacheline 45 boundary (2880 bytes) --- */
struct thread_struct thread __attribute__((__aligned__(64))); /* 2880 4288 */
/* size: 7168, cachelines: 112, members: 155 */
/* sum members: 7148, holes: 2, sum holes: 12 */
/* sum bitfield members: 7 bits, bit holes: 2, sum bit holes: 57 bits */
/* paddings: 1, sum paddings: 2 */
/* forced alignments: 6, forced holes: 2, sum forced holes: 12 */
} __attribute__((__aligned__(64)));
-
task_struct 结构体:是 Linux 内核中用于表示进程的核心数据结构。它包含了与进程相关的所有信息,包括进程状态、调度信息、线程信息等。
-
thread_info:包含线程相关的控制信息,如内核栈指针、状态标志等。
-
state:表示进程当前的状态,如运行、就绪、阻塞等。
-
stack:指向进程堆栈的指针,内核栈用于保存该进程在内核态运行时的局部变量与函数调用。
-
thread_struct:该结构体包含与线程调度相关的信息,如调度优先级、状态、运行时间等。
📊 结构体大小分析
-
task_struct 的大小接近 8 8 8KB,具体为 7168 7168 7168 字节。
-
它包含 155 155 155 个成员,这些成员分布在多个缓存行(cacheline)上,以优化性能。
-
结构体的字段有很多填充(padding)和对齐(alignment),这是为了优化内存访问。
💡 对齐和填充
结构体使用了 attribute((aligned(64))) 来强制对齐,保证结构体的起始地址是 64 64 64 字节对齐的。
这种对齐方式优化了内存访问,尤其是在多核处理器系统中,能够提高缓存命中率,减少不必要的缓存行竞争。
测验:检查进程以确定打开的文件
使用调试器检查名为 syslogd
的进程。
问题 1:我们应该使用什么命令来列出打开的文件描述符?
lsof -p <pid>
其中,<pid>
是进程 ID。lsof
命令会列出指定进程打开的所有文件描述符。
问题 2:有多少个文件描述符被打开?
lsof 命令将显示所有打开的文件描述符及其对应的文件,输出的条目数量即为打开的文件描述符数量。例如:
$ lsof -p 1234
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
syslogd 1234 root cwd DIR 8,1 4096 123 /var
syslogd 1234 root 0u CHR 136,2 0t0 10 /dev/tty1
syslogd 1234 root 1u CHR 136,2 0t0 10 /dev/tty1
syslogd 1234 root 2u CHR 136,2 0t0 10 /dev/tty1
问题 3:我们应该使用什么命令来确定打开的文件描述符 3 对应的文件名?
ls -l /proc/1234/fd/3
问题 4:文件描述符 3 对应的文件名是什么?
$ ls -l /proc/1234/fd/3
lrwx------ 1 root root 64 Mar 14 12:34 /proc/1234/fd/3 -> /var/log/syslog
🧠 重要概念解释:
-
文件描述符(File Descriptor):在 UNIX 和 Linux 系统中,文件描述符是一个用于标识已打开文件的整数。每个进程都有一个文件描述符表,用于管理该进程打开的文件。
-
lsof 命令:lsof 是一个显示打开文件和文件描述符的工具,它可以列出进程打开的文件、网络连接等。
-
/proc/
<pid>
/fd/:该目录包含了进程打开的所有文件描述符的符号链接,指向相应的文件或设备。
线程(Threads)
线程是内核调度程序使用的基本单元,用于允许应用程序运行在 CPU 上。一个线程具有以下特征:
线程特征
-
每个线程都有自己的堆栈,并且与寄存器值一起决定了线程的执行状态。
- 每个线程都有独立的栈空间,用于存储函数调用、局部变量和返回地址等。
-
线程在进程的上下文中运行,同一进程中的所有线程共享该进程的资源。
- 线程是进程的一部分,它们共享进程的地址空间、文件描述符等资源,但每个线程有自己的执行栈和寄存器状态。
-
内核调度的是线程,而不是进程,并且用户级线程(例如:纤程、协程等)对内核不可见。
- 内核调度程序关注的是线程的调度,而不是进程。虽然进程包含多个线程,但内核关注的是每个线程的执行,而非整个进程。
-
线程的典型实现:线程通常作为一个独立的数据结构实现,并与进程的数据结构相连接。
- 例如,Windows 内核就是使用这种实现方式:每个线程是一个独立的实体,通过指针与进程的任务结构体相连接。
🧠 关键概念解释:
- 线程(Thread):是操作系统执行的基本单位,线程由内核调度并与其他线程共享进程资源。它比进程更加轻量,因此可以在同一个进程中创建多个线程并发执行。
- 进程(Process):是资源分配的基本单位,它包含一个或多个线程,拥有独立的地址空间。
- 内核调度(Kernel Scheduling):操作系统内核通过调度程序决定哪个线程在某个时刻获取 CPU 资源。内核调度通常基于线程的优先级、时间片和其他因素。
- 用户级线程(User-Level Threads):是由用户空间的库或程序调度的线程,通常不被内核直接管理。例如,Java 中的协程或纤程(fibers)就是这种类型的线程,它们不会显示在内核的调度器中。
这张图展示了windows kernel中进程( E P R O C E S S EPROCESS EPROCESS)与线程( E T H R E A D ETHREAD ETHREAD 和 K T H R E A D KTHREAD KTHREAD)之间的关系和结构。
🧑💻 进程结构( E P R O C E S S EPROCESS EPROCESS)
在操作系统中,每个进程都有一个相应的进程结构体( E P R O C E S S EPROCESS EPROCESS),它包含进程的所有信息和资源:
- K P R O C E S S KPROCESS KPROCESS:进程的核心数据结构,包含进程的各种信息。
- 进程 ID(PID):每个进程都有一个唯一的标识符,用于区分不同的进程。
- 线程列表(Thread list):一个进程可以有多个线程,线程列表用于存储与进程相关的所有线程。
- 打开的文件(Opened files):进程所打开的文件,包括文件描述符等信息。
- 地址空间(Address Space):进程的虚拟地址空间,包含该进程的所有代码、数据、堆栈等。
🧵 线程结构( E T H R E A D ETHREAD ETHREAD 和 K T H R E A D KTHREAD KTHREAD)
线程(Thread)是进程中的基本执行单元。每个线程都有一个相应的线程结构体( E T H R E A D ETHREAD ETHREAD 或 K T H R E A D KTHREAD KTHREAD),它们存储线程的具体信息:
- E T H R E A D ETHREAD ETHREAD:表示用户空间中的线程(例如,应用程序的线程),它与进程的线程列表相关联。每个 E T H R E A D ETHREAD ETHREAD 结构体包含线程的状态、栈信息等。
- K T H R E A D KTHREAD KTHREAD:表示内核中的线程,它与进程的内核线程相关联。每个 K T H R E A D KTHREAD KTHREAD 结构体包含线程 ID(TID)、线程起始地址等信息。
每个线程都拥有独立的 线程 ID(TID) 和 线程起始地址(Thread Start Address),它们的资源(如地址空间)与所属进程共享。
🧠 关键概念解释
- E P R O C E S S EPROCESS EPROCESS:表示操作系统中的一个进程结构体,包含了所有与进程相关的信息和资源。
- K P R O C E S S KPROCESS KPROCESS:进程内部的核心结构体,存储进程的状态、资源等信息。
- E T H R E A D ETHREAD ETHREAD:用户空间线程的结构体,包含线程的状态、堆栈等信息。
- K T H R E A D KTHREAD KTHREAD:内核空间线程的结构体,包含线程的 ID 和线程起始地址等。
- 线程 ID(TID):每个线程的唯一标识符,用于区分同一进程中的不同线程。
Linux 线程的实现
Linux 使用不同的方式来实现线程。基本单元被称为任务(task),因此有了 $struct\ task_struct$
结构体,它既用于表示线程,也用于表示进程。
📌 任务(task)结构体
在 Linux 中,进程和线程的管理是通过任务(task)来实现的。与传统的线程实现不同,Linux 将资源(如地址空间、文件描述符等)作为指针存储在 $struct\ task_struct$
中,而不是将资源直接嵌入结构体中。
- 这样,如果两个线程属于同一个进程,它们会指向相同的资源结构实例。
- 如果两个线程分别属于不同的进程,它们会指向不同的资源结构实例。
🧠 关键概念解释
-
s
t
r
u
c
t
t
a
s
k
s
t
r
u
c
t
struct\ task_struct
struct taskstruct:在 Linux 中,进程和线程都由
$struct\ task_struct$
结构体表示。该结构体包含任务的所有调度信息、状态信息等。任务结构体通过指向其他资源的指针来引用与进程或线程相关的资源。 - 任务(task):在 Linux 中,任务是进程和线程的抽象单元,内核调度的基本单位。每个任务都由一个
$task_struct$
结构体表示。 - 资源指针:在
$task_struct$
中,资源如地址空间、文件描述符等并没有直接嵌入其中,而是通过指针指向单独的资源结构。这样可以减少内存消耗,提高效率,同时确保不同进程和线程能够共享或独立拥有资源。
通过这种设计,Linux 能够高效地管理进程和线程,充分利用资源,并通过调度器为每个任务分配 CPU 时间。
这张图展示了 Linux 中进程结构($task_struct$
)与其资源(如已打开的文件、地址空间等)之间的关系。
📌 进程与资源结构
在 Linux 中,每个进程由一个 $task_struct$
结构体表示。进程结构体包含进程的基本信息,包括线程 ID、线程组 ID、已打开的文件、地址空间等。
$task_struct$
:表示一个任务(进程或线程)的数据结构,它包含了进程的状态、ID、资源等信息。- 线程组 ID(PID):表示进程组的标识符,多个线程可以共享相同的 PID。
- 线程 ID(TID):表示线程的唯一标识符,每个线程都有一个不同的 TID。
- 打开的文件(Opened files):进程所打开的文件的列表,通常会用文件描述符进行标识。
- 地址空间(Address Space):进程的虚拟内存空间,包括代码段、数据段、堆、栈等。
🔄 进程与资源的共享
- 共享的资源:如果多个线程属于同一个进程,它们将共享该进程的地址空间和打开的文件等资源。
- 例如,两个线程(属于同一个进程)会共享一个地址空间和同一组已打开的文件描述符。
- 独立的资源:尽管线程共享进程的资源,每个线程都有自己的寄存器、堆栈和线程 ID。
🧠 关键概念解释
$task_struct$
:Linux 内核中表示进程和线程的结构体,包含进程的调度信息、资源、状态等。每个进程或线程都有一个独立的$task_struct$
。- 线程组 ID(PID):进程的唯一标识符,用于区分不同的进程。线程组内的所有线程共享同一个 PID。
- 线程 ID(TID):线程的唯一标识符,用于区分同一进程中的不同线程。
- 文件描述符(File Descriptors):操作系统用来标识进程所打开的文件的整数值。每个进程都有一个文件描述符表。
- 地址空间(Address Space):每个进程都有独立的虚拟地址空间,用于存放进程的代码、数据、堆和栈等信息。线程共享进程的地址空间。