Linux进程与线程
1. 简介
您是否曾经对操作系统中进程和线程 之间的区别感到困惑?在本文中,我们将讨论 Linux 上下文中的进程和线程的细节。
2. 过程
进程是正在执行的计算机程序。Linux 在任何给定时间都在运行许多进程。我们可以使用ps 命令在终端上或在System Monitor UI 上监控它们。例如,让我们看一个使用ps命令查看机器上运行的所有进程的示例:
[blogdemo@itcodingman ~]$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jun28 ? 00:00:16 /usr/lib/systemd/systemd --switched-root --system --deserialize 31
root 2 0 0 Jun28 ? 00:00:00 [kthreadd]
root 3 2 0 Jun28 ? 00:00:00 [rcu_gp]
root 4 2 0 Jun28 ? 00:00:00 [rcu_par_gp]
root 6 2 0 Jun28 ? 00:00:04 [kworker/0:0H-kblockd]
root 8 2 0 Jun28 ? 00:00:00 [mm_percpu_wq]
root 9 2 0 Jun28 ? 00:00:00 [rcu_tasks_kthre]
root 10 2 0 Jun28 ? 00:00:00 [rcu_tasks_rude_]
root 11 2 0 Jun28 ? 00:00:00 [rcu_tasks_trace]
root 12 2 0 Jun28 ? 00:00:11 [ksoftirqd/0]
root 13 2 0 Jun28 ? 00:01:24 [rcu_sched]
root 14 2 0 Jun28 ? 00:00:00 [migration/0]
root 16 2 0 Jun28 ? 00:00:00 [cpuhp/0]
root 17 2 0 Jun28 ? 00:00:00 [cpuhp/1]
当我们运行新命令/应用程序或完成旧命令时,我们可以看到进程数量动态增长和收缩。Linux 进程是隔离的,不会中断彼此的执行。
使用 PID,我们可以识别 Linux 中的任何进程。在内部,内核唯一地分配这个数字并在进程退出后释放它以供重用。我们可以将 PID 视为上述ps命令输出的第二列。
由于在 Linux 中任何给定时间都在运行许多进程,因此它们必须共享 CPU。CPU 上两个正在执行的进程之间切换的过程称为进程上下文切换。进程上下文切换代价高昂,因为内核必须保存旧寄存器并加载当前寄存器、内存映射和其他资源。
3. 线程
线程是一个轻量级进程。通过创建一个或多个线程,一个进程可以同时执行多个工作单元。这些线程是轻量级的,可以快速生成。
让我们看一个示例,并使用ps -eLf命令识别 Linux 中的进程及其线程。我们对 PID、LWP 和 NLWP 属性感兴趣:
- PID:唯一的进程标识符
- LWP:进程内的唯一线程标识符
- NLWP:给定进程的线程数
[blogdemo@itcodingman ~]$ ps -eLf
UID PID PPID LWP C NLWP STIME TTY TIME CMD
root 1 0 1 0 1 Jun28 ? 00:00:16 /usr/lib/systemd/systemd --switched-root --system --deserialize 31
root 2 0 2 0 1 Jun28 ? 00:00:00 [kthreadd]
root 3 2 3 0 1 Jun28 ? 00:00:00 [rcu_gp]
root 4 2 4 0 1 Jun28 ? 00:00:00 [rcu_par_gp]
root 6 2 6 0 1 Jun28 ? 00:00:05 [kworker/0:0H-acpi_thermal_pm]
root 8 2 8 0 1 Jun28 ? 00:00:00 [mm_percpu_wq]
root 12 2 12 0 1 Jun28 ? 00:00:11 [ksoftirqd/0]
root 13 2 13 0 1 Jun28 ? 00:01:30 [rcu_sched]
root 14 2 14 0 1 Jun28 ? 00:00:00 [migration/0]
root 690 1 690 0 2 Jun28 ? 00:00:00 /sbin/auditd
root 690 1 691 0 2 Jun28 ? 00:00:00 /sbin/auditd
root 709 1 709 0 4 Jun28 ? 00:00:00 /usr/sbin/ModemManager
root 709 1 728 0 4 Jun28 ? 00:00:00 /usr/sbin/ModemManager
root 709 1 729 0 4 Jun28 ? 00:00:00 /usr/sbin/ModemManager
root 709 1 742 0 4 Jun28 ? 00:00:00 /usr/sbin/ModemManager
我们可以通过它们的 NLWP 值轻松识别单线程和多线程进程。PID 690 和 709 的 NLWP 分别为 2 和 4。因此,它们是多线程的,有 2 个和 4 个线程。所有其他进程的 NLWP 为 1 并且是单线程的。
仔细观察,我们可以看到单线程进程具有相同的 PID 和 LWP 值,就好像它们是同一个东西一样。但是,在多线程进程中,只有一个 LWP 与其 PID 匹配,其他 LWP 的 LWP 值不同。另外,请注意,该值一旦分配给 LWP,就永远不会分配给另一个进程。
3.1. 单线程进程
**在进程中创建的任何线程都共享该进程的相同内存和资源。在单线程进程中,进程和线程是相同的,因为只有一件事发生。**我们还可以验证我们之前讨论的ps -eLf输出,即 PID 和 LWP 对于单线程进程是相同的。
3.2. 多线程进程
在多线程进程中,该进程有多个线程。这样的过程同时或几乎同时完成多项任务。
众所周知,**线程共享进程的相同地址空间。因此,与启动新进程相比,在进程中生成新线程变得便宜(就系统资源而言)。**与 CPU 中的进程相比,线程也可以更快地切换(因为它们与进程共享地址空间)。在内部,线程在内存中只有一个栈,它们与父进程共享堆(进程内存)。
由于线程的这种性质,我们也将其称为轻量级进程(LWP)。
与其他线程共享相同的内存既有好处也有坏处。
最重要的好处是我们可以比进程更快地创建线程,因为我们不必分配内存和资源。另一个好处是线程间通信的低成本。
与进程上下文切换类似,还有线程上下文切换的概念。线程上下文切换更快,因为线程在切换发生之前只记录其堆栈值。
有一个主要缺点:由于线程共享相同的内存,如果进程执行许多并发任务,它们可能会变慢。
4. 流程内部
在本节中,我们将研究 Linux 进程的内部结构及其实现。
4.1. 系统调用
Linux 为文件管理、网络管理、进程管理等基本操作系统功能定义了系统调用。任何有效的 Linux 程序都使用这些系统调用。因此,为了便于应用程序开发,GNU C 库将它们公开为 API。
我们使用fork (或clone )和execve 系统调用在 Linux 中创建进程。在这里,fork系统调用创建了一个与父进程等效的子进程。execve系统调用替换子进程的可执行文件。在现代实现中,fork系统调用在内部使用clone系统调用。因此,我们将更多地关注clone系统调用。
4.2. 内部结构
作为 Linux 系统用户,我们永远不必为进程创建内部数据结构。但是,了解 Linux 进程的内部数据结构至关重要。Linux 使用 C 中名为task_struct 的数据结构创建每个进程。Linux 内核将它们保存在一个动态列表中,以表示所有正在运行的进程,称为任务列表。在这个tasklist中,每个元素都是task_struct类型,它描述了一个 Linux 进程。
我们将任务结构中的各个字段分类为调度参数、内存映像、信号、机器寄存器、系统调用状态、文件描述符、内核堆栈等。因此,当我们创建一个新进程时,Linux 内核会在内核内存中创建一个新的task_struct,指向新创建的进程 。
4.3. 创作流程
让我们跟踪ls命令进程,以可视化进程创建流程。我们将使用strace 命令来跟踪调用:
[blogdemo@itcodingman ~]$ strace -f -etrace=execve,clone bash -c '{ ls; }'
execve("/usr/bin/bash", ["bash", "-c", "{ ls; }"], 0x7fff153d0ed0 /* 36 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 115538 attached
, child_tidptr=0x7fe8b4f10a10) = 115538
[pid 115538] execve("/usr/bin/ls", ["ls"], 0x55eed8e42be0 /* 36 vars */) = 0
Desktop Documents Downloads Dropbox IdeaProjects Music Pictures Public Templates Videos revision soft
[pid 115538] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=115538, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
我们可以从上面的输出中观察到**,创建一个流程需要两个步骤**:
- clone系统调用将进程创建为bash进程的克隆
- 然后execve系统调用将进程中的可执行文件替换为ls命令二进制文件
另一个重要的一点是,即使我们说子进程是具有相同地址空间的父进程的副本/克隆,但在实现方面,Linux 在子进程写入之前不会复制子进程的内存。Linux 中这种巧妙的进程实现节省了 RAM 空间并避免了不必要的内存分配。此实现也称为写时复制 (COW)。
4.4. 进程树
在上面的strace结果中,我们注意到一个进程在execve执行bash -c ls之后内部调用了clone。此流程表明bash进程是ls命令的父进程。
类似地,父进程在 Linux 中创建除了 PID 1 (INIT process) 之外的每个进程。*pstree *命令帮助我们可视化进程层次结构:
[blogdemo@itcodingman ~]$ pstree
systemd─┬─ModemManager───3*[{ModemManager}]
├─NetworkManager───2*[{NetworkManager}]
├─accounts-daemon───3*[{accounts-daemon}]
├─2*[agetty]
├─alsactl
├─auditd───{auditd}
├─avahi-daemon───avahi-daemon
├─bluetoothd
├─chronyd
├─colord───3*[{colord}]
├─cupsd
├─dbus-broker-lau───dbus-broker
├─firewalld───{firewalld}
├─gdm─┬─gdm-session-wor─┬─gdm-wayland-ses─┬─gnome-session-b───3*[{gnome-session-b}]
│ │ │ └─2*[{gdm-wayland-ses}]
│ │ └─2*[{gdm-session-wor}]
│ └─2*[{gdm}]
├─gnome-keyring-d───3*[{gnome-keyring-d}]
├─gssproxy───5*[{gssproxy}]
├─low-memory-moni───2*[{low-memory-moni}]
5. 螺纹内部
与进程一样,我们使用clone系统调用来创建线程。克隆系统调用非常通用。我们甚至可以根据定义创建既不是进程也不是线程的东西。让我们看一下克隆系统调用的签名:
pid = clone(function, stack ptr, sharing flags, arg);
克隆系统调用使用我们提供的*CLONE标志*来确定进程或线程的创建**:
我们将使用上表 作为参考,使用克隆系统调用创建进程或线程。
例如,如果我们想创建一个线程,我们设置CLONE_VM标志。同样,要创建一个进程,我们取消设置CLONE_VM标志。
通过提供CLONE_VM标志,我们指示克隆命令与子进程共享父进程内存。还有其他标志,当设置时,使用共享文件系统信息 ( CLONE_FS )、打开的文件 ( CLONE_FILES )等创建新线程或其变体。
由于clone具有不同的标志,我们可以以各种方式使用它,因此我们有用于在所有 Unix 和 Linux 变体中创建可移植线程的实现标准。
POSIX(便携式操作系统接口)就是这样一种标准,它为 Unix、Linux 及其变体中的兼容性定义了 API、shell 命令和实用程序接口。出于可移植性原因,我们建议使用来自 POSIX 的pthread_create ** API 调用。
6. 进程与线程的区别
让我们回顾一下Linux上下文中进程和线程之间的区别:
过程 | 线程 |
---|---|
一个进程是重量级的。 | 线程是一个轻量级进程,也称为 LWP。 |
进程有自己的内存。 | 线程与父进程和进程内的其他线程共享内存。 |
由于内存隔离,进程间通信较慢。 | 由于共享内存,线程间通信更快。 |
由于保存旧的和加载新的进程内存和堆栈信息,进程之间的上下文切换是昂贵的。 | 由于共享内存,线程之间的上下文切换成本更低。 |
当内存不足时,其组件具有多个进程的应用程序可以提供更好的内存利用率。我们可以为应用程序中的非活动进程分配低优先级。然后这个空闲进程就有资格被交换到磁盘。这使应用程序的活动组件保持响应。 | 当内存不足时,多线程应用程序不提供任何管理内存的规定。 |