跳至内容

多任务功能

来自维基教科书,开放世界中的开放书籍


多任务处理
进程
线程或任务
同步
调度程序
中断核心
CPU 特定

Linux 内核是一个抢占式 多任务 操作系统。作为一个多任务操作系统,它允许多个进程共享处理器 (CPU) 和其他系统资源。每个 CPU 每次执行一个任务。但是,多任务处理允许每个处理器在执行的任务之间切换,而无需等待每个任务完成。为此,内核可以在任何时间暂时中断处理器正在执行的任务,并用另一个任务替换它,该任务可以是新的任务,也可以是先前挂起的任务。涉及运行任务交换的操作称为上下文切换


进程是运行中的用户空间程序。内核在函数 run_init_process id 中使用 kernel_execve id 启动第一个进程 /sbin/init。进程占用系统资源,例如内存、CPU 时间。系统调用 sys_fork idsys_execve id 用于从用户空间创建新进程。进程使用 sys_exit id 系统调用退出。

Linux 从 Unix 继承了其基本进程管理系统调用 (⚲ API ↪ ⚙️ 实现)

man 2 forkkernel_clone id 通过 复制 调用它的进程来创建一个新进程。

man 2 _exitdo_exit id “立即”终止调用进程。属于该进程的任何打开的文件描述符都将关闭。

man 2 waitkernel_waitid id 挂起调用进程的执行,直到其子进程之一终止。

man 2 execvedo_execve id 在当前进程的上下文中运行可执行文件,替换先前的可执行文件。此系统调用由 libc man 3 exec 函数族使用。

Linux 使用其自己的系统调用 man 2 clone 来增强传统的 Unix 进程 API。克隆创建一个子进程,该子进程可以与父进程共享其执行上下文的一部分。它通常用于实现线程(尽管程序员通常会使用更高级别的接口,例如 man 7 pthreads,它是在克隆之上实现的)。


PID - 进程标识符 定义为 pid_t id 是唯一的顺序编号。 man 1 ps -A 列出当前进程。系统调用 man 2 getpidtask_tgid_vnr id 返回当前进程的 PID,在内部称为 TGID - 线程组 ID。一个进程可以包含多个线程。 man 2 gettidtask_pid_vnr id 返回线程 ID。在内部历史上称为 PID。⚠️ 警告:混淆。用户空间 PID ≠ 内核空间 PID。 man 1 ps -AF 列出当前进程和线程作为 LWP。对于单线程进程,所有这些 ID 都是相等的。


⚲ API

unistd.h
sys/types.h
sys/wait.h


⚙️ 内部

task_structid
pid_typeid
kernel/fork.csrc
syscalls
man 2 set_tid_address – 设置指向线程 ID 的指针
man 2 fork – 创建子进程
man 2 vfork – 创建子进程并阻塞父进程
man 2 clone – 创建子进程
man 2 unshare – 取消关联进程执行上下文的部分
kernel/sys.csrc
syscalls
man 2 prctl – 对进程或线程的操作
kernel/pid.csrc
syscalls
man 2 pidfd_open – 获取指向进程的文件描述符
man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
syscalls
man 2 pidfd_open – 获取指向进程的文件描述符
man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
kernel/exit.csrc
syscalls
man 2 exit – 终止调用进程
man 2 exit_group – 退出进程中的所有线程
man 2 waitid – 等待进程状态改变
man 2 waitpid – 等待进程状态改变


fs/exec.csrc


📖 参考资料

fork (系统调用)
exit (系统调用)
wait (系统调用)
exec (系统调用)

进程间通信

[edit | edit source]

进程间通信 (IPC) 特指操作系统提供的机制,用于允许其管理的进程共享数据。实现 IPC 的方法分为几类,它们根据软件需求(如性能和模块化需求)以及系统环境而有所不同。Linux 从 Unix 继承了以下 IPC 机制

信号 (⚲ API ↪ ⚙️ 实现)

man 2 kill 向进程发送信号
man 2 tgkilldo_tkill id 向线程发送信号
man 2 process_vm_readvprocess_vm_rw id - 进程地址空间之间零拷贝数据传输

🔧 TODO: man 2 sigaction man 2 signal man 2 sigaltstack man 2 sigpending man 2 sigprocmask man 2 sigsuspend man 2 sigwaitinfo man 2 sigtimedwait

kernel/signal.csrc


匿名管道 和命名管道 (FIFO) man 2 mknoddo_mknodat id S_IFIFO id
快速数据路径 PF_XDP id
Unix 域套接字 PF_UNIX id
内存映射文件 man 2 mmapksys_mmap_pgoff id
Sys V IPC
消息队列
信号量
共享内存: man 2 shmget, man 2 shmctl, man 2 shmat, man 2 shmdt


📖 参考资料

进程间通信
man 7 sysvipc

线程或任务

[edit | edit source]

在 Linux 内核中,“线程”和“任务”几乎是同义词。

💾 历史:直到 2.6.39,内核模式只有一个线程,由 大内核锁 保护。


⚲ API

linux/sched.h inc - 主要调度程序 API
task_structid
arch/x86/include/asm/current.hsrc
current idget_current id () 返回当前 task_struct id
uapi/linux/taskstats.h inc 每个任务的统计信息
linux/thread_info.hinc
函数 current_thread_info id() 返回 thread_info id
linux/sched/task.h inc - 调度程序和各种任务生命周期 (fork()/exit()) 功能之间的接口
linux/kthread.h inc - 用于创建和停止内核线程的简单接口,无需麻烦。
kthread_run id 创建并唤醒线程
kthread_createid


⚙️ 内部

kthread_run id ↯ 层次结构
kernel_threadid
kernel_cloneid
kernel/kthread.csrc

调度程序

[edit | edit source]

调度程序 是操作系统中决定在特定时间点运行哪个进程的部分。它通常能够暂停正在运行的进程,将其移到运行队列的末尾,并启动一个新进程。

活动进程被放置在一个称为运行队列 的数组中,或运行队列 - rq id。运行队列可能包含每个进程的优先级值,调度程序将使用这些值来确定下一个运行哪个进程。为了确保每个程序都能公平地共享资源,每个程序都会运行一段时间(时间片),然后暂停并放回运行队列中。当一个程序停止让另一个程序运行时,运行队列中优先级最高的程序将被允许执行。当进程请求睡眠、等待资源可用或被终止时,它们也会从运行队列中移除。

Linux 使用 完全公平调度程序 (CFS),这是第一个在通用操作系统中广泛使用的公平队列进程调度程序的实现。CFS 使用一种经过充分研究的经典调度算法,称为“公平队列”,最初是为分组网络发明的。CFS 调度程序的调度复杂度为 O(log N),其中 N 是运行队列中的任务数。选择任务可以在恒定时间内完成,但是任务运行后重新插入需要 O(log N) 操作,因为运行队列是用 红黑树 实现的。

与之前的 O(1) 调度程序 相比,CFS 调度程序的实现不是基于运行队列。相反,红黑树实现了一个“时间线”,用于描述未来任务的执行。此外,调度程序使用纳秒级粒度记账,这是分配给单个进程的 CPU 使用份额的原子单位(因此使得之前的时间片概念变得多余)。这种精确的知识也意味着不需要特定的启发式方法来确定进程的交互性,例如。

与旧的 O(1) 调度程序一样,CFS 使用一个称为“睡眠公平”的概念,它将睡眠或等待的任务视为与运行队列中的任务等效。这意味着,当交互式任务需要 CPU 时间时,它们可以获得与在用户输入或其他事件等待期间花费的大部分时间相似的 CPU 时间份额。

用于调度算法的数据结构是红黑树,其中节点是特定于调度程序的结构,称为 sched_entity id。 这些是从通用task_struct进程描述符派生的,并添加了调度程序元素。 这些节点按纳秒级的处理器执行时间索引。 每个进程还计算出最大执行时间。 此时间基于这样一种想法,即“理想处理器”将在所有进程之间平等地共享处理能力。 因此,最大执行时间是进程等待运行的时间除以进程总数,或者换句话说,最大执行时间是进程在“理想处理器”上预期运行的时间。

当调用调度程序来运行新进程时,调度程序的操作如下:

  1. 选择调度树的最左节点(因为它将具有最低的已消耗执行时间),并将其发送以执行。
  2. 如果进程简单地完成执行,它将从系统和调度树中删除。
  3. 如果进程达到其最大执行时间或以其他方式停止(自愿或通过中断),则它将根据其新的已消耗执行时间重新插入到调度树中。
  4. 然后将从树中选择新的最左节点,重复迭代。

如果进程将其大部分时间花费在休眠上,则其已消耗时间值很低,并且当它最终需要时,它会自动获得优先级提升。 因此,此类任务不会比不断运行的任务获得更少的处理器时间。

CFS 的替代方法是 Con Kolivas 创建的 Brain Fuck Scheduler (BFS)。 与其他调度程序相比,BFS 的目标是提供一个具有更简单算法的调度程序,该算法不需要调整启发式算法或调整参数来使性能适应特定类型的计算工作负载。

Con Kolivas 还维护着 CFS 的另一个替代方案,即 MuQSS 调度程序。[1]

Linux 内核包含不同的调度程序类(或策略)。 目前默认使用的完全公平调度程序是 SCHED_NORMAL id 调度程序类,也称为 SCHED_OTHER。 内核还包含另外两个类 SCHED_BATCH idSCHED_IDLE id,以及另外两个实时调度程序类,名为 SCHED_FIFO id(实时先入先出)和 SCHED_RR id(实时循环调度),以及第三个实时调度策略,称为 SCHED_DEADLINE id,它实现了 最早截止期限优先算法 (EDF),该算法稍后添加。 任何实时调度程序类都优先于任何“正常”(即非实时)类。 调度程序类是通过 man 2 sched_setschedulerdo_sched_setscheduler id 系统调用选择和配置的。

在调度程序中适当地平衡延迟、吞吐量和公平性是一个开放性问题。[1]


⚲ API

man 1 renice – 运行进程的优先级
man 1 nice – 以修改后的调度优先级运行程序
man 1 chrt – 操作进程的实时属性
man 2 sched_getattrsys_sched_getattr id – 获取调度策略和属性
linux/sched.h inc – 主要调度程序 API
scheduleid
man 2 getpriorityman 2 setpriority
man 2 sched_setschedulerman 2 sched_getscheduler


⚙️ 内部

sched_init idstart_kernel id 调用
__schedule id 是主要的调度程序函数。
runqueues idthis_rq id
kernel/schedsrc
kernel/sched/core.csrc
kernel/sched/fair.c src 实现 SCHED_NORMAL idSCHED_BATCH idSCHED_IDLE id
sched_setscheduler idsched_getscheduler id
task_struct id::rt_priority id 以及其他具有不太独特标识符的成员


🛠️ 工具

man 1 pidstat]
man 1 pcp-pidstat
man 1 perf-sched
使用 SchedViz 了解调度行为


📖 参考资料

man 7 sched
调度 doc
CFS
完全公平调度程序 doc
CFS 带宽控制 doc
调整任务调度程序
停止在 Kubernetes 上使用 CPU 限制
完全公平调度程序 LWN
截止日期任务调度程序 doc
sched ltp
sched_setparam ltp
sched_getscheduler ltp
sched_setscheduler ltp


📚 关于调度程序的进一步阅读

调度程序跟踪
bcc/ebpf CPU 和调度程序工具


抢占

[edit | edit source]

抢占是指系统中断正在运行的任务以切换到另一个任务的能力。 这是确保高优先级任务获得必要的 CPU 时间并提高系统响应能力所必需的。 在 Linux 中,抢占模型定义了内核如何以及何时抢占任务。 不同的模型在系统响应能力和吞吐量之间提供了不同的权衡。

📖 参考资料

kernel/Kconfig.preemptsrc
CONFIG_PREEMPT_NONE id – 服务器没有强制抢占
CONFIG_PREEMPT_VOLUNTARY id – 桌面自愿抢占
CONFIG_PREEMPT id – 除关键部分外,低延迟桌面可抢占
CONFIG_PREEMPT_RT id – 实时抢占,用于 高度响应的应用程序
CONFIG_PREEMPT_DYNAMIC id,请参见 /sys/kernel/debug/sched/preempt

等待队列

[edit | edit source]

内核中的等待队列是一种数据结构,它允许一个或多个进程等待(休眠),直到发生感兴趣的事情。它们在整个内核中被用来等待可用内存、I/O 完成、消息到达以及许多其他事情。在 Linux 的早期,等待队列只是一个简单的等待进程列表,但各种可扩展性问题(包括惊群问题)导致从那时起添加了相当多的复杂性。


⚲ API

linux/wait.hinc

wait_queue_head idwait_queue_entry id 的双向链表和一个自旋锁组成。

等待简单事件

使用两种方法之一来初始化 wait_queue_head id
init_waitqueue_head id 在函数上下文中初始化 wait_queue_head id
DECLARE_WAIT_QUEUE_HEAD id - 实际上在全局上下文中定义 wait_queue_head id
等待替代方案
wait_event_interruptible id - 首选等待
wait_event_interruptible_timeoutid
wait_event id - 不可中断等待。可能导致死锁 ⚠
wake_up id

👁 例如,请参阅对唯一的 suspend_queue id 的引用。

在复杂情况下,显式使用 add_wait_queue 而不是简单的 wait_event

DECLARE_WAITQUEUE id 实际上使用 default_wake_function id 定义 wait_queue_entry
add_wait_queue id 将进程插入等待队列的第一个位置
remove_wait_queueid


⚙️ 内部

___wait_eventid
__add_wait_queueid
__wake_up_common idtry_to_wake_up id
kernel/sched/wait.csrc


📖 参考资料

等待队列和唤醒事件 doc
处理等待队列

同步

[edit | edit source]

线程同步被定义为一种机制,它确保两个或多个并发进程或线程不会同时执行某个特定的程序段,称为互斥(互斥)。当一个线程开始执行临界区(程序的序列化段)时,另一个线程应该等待,直到第一个线程完成。如果没有应用适当的同步技术,它可能会导致竞争条件,其中变量的值可能是不可预测的,并且会根据进程或线程的上下文切换时间而变化。

用户空间同步

[edit | edit source]

Futex

[edit | edit source]

一个 man 2 futexdo_futex id(“快速用户空间互斥体”的缩写)是一个内核系统调用,程序员可以使用它来实现基本锁定,或作为更高级别锁定抽象的构建块,例如信号量和 POSIX 互斥体或条件变量。

Futex 由一个内核空间等待队列组成,该队列附加到用户空间中的对齐整数。多个进程或线程完全在用户空间中操作该整数(使用原子操作以避免相互干扰),并且仅在请求等待队列上的操作(例如唤醒等待进程或将当前进程放入等待队列)时才诉诸于相对昂贵的系统调用。经过正确编程的基于 futex 的锁将不会使用系统调用,除非锁存在争用;由于大多数操作不需要进程之间的仲裁,因此在大多数情况下不会发生这种情况。


futex 的基本操作仅基于两个核心操作 futex_wait idfutex_wake id,尽管实现对于更多专门情况有更多操作。

WAIT(addrval)检查存储在地址addr处的 value 是否为val,如果是,则将当前线程置于休眠状态。
WAKE(addrval)唤醒等待地址addrval个线程。


⚲ API

uapi/linux/futex.hinc
linux/futex.hinc

⚙️ 内部结构:kernel/futex.c src

📖 参考资料

Futex
man 7 futex
Futex API 参考 doc
futex ltp


文件锁定

[edit | edit source]

⚲ API:man 2 flock


信号量

[edit | edit source]

💾 历史:信号量是 System V IPC man 7 sysvipc 的一部分

⚲ API

man 2 semget
man 2 semctl
man 2 semget


⚙️ 内部结构:ipc/sem.c src

内核空间同步

[edit | edit source]

对于内核模式同步,Linux 提供了三类锁定原语:休眠、每个 CPU 本地锁和自旋锁。

休眠锁

[edit | edit source]
读-复制-更新
[edit | edit source]

解决读者-写者问题的常用机制是读-复制-更新RCU)算法。读-复制-更新实现了一种对读者无等待(非阻塞)的互斥,允许非常低的开销。但是,RCU 更新可能很昂贵,因为它们必须将数据结构的旧版本保留在适当的位置,以适应现有的读者。

💾 历史:RCU 于 2002 年 10 月添加到 Linux。从那时起,内核中存在着数千种 RCU API 的使用,包括网络协议栈和内存管理系统。Linux 内核 2.6 版本中 RCU 的实现是最知名的 RCU 实现之一。


linux/rcupdate.h inc 中的核心 API 很小

rcu_read_lock id 标记受 RCU 保护的数据结构,以便在该临界区持续期间不会回收它。
rcu_read_unlock id 用于读者告知回收器读者已退出 RCU 读取端临界区。请注意,RCU 读取端临界区可以嵌套和/或重叠。
synchronize_rcu id 会阻塞,直到所有 CPU 上所有现有的 RCU 读取端临界区都已完成。请注意,synchronize_rcu 不会必然等待任何后续的 RCU 读取端临界区完成。


👁 例如,考虑以下事件序列

	         CPU 0                  CPU 1                 CPU 2
	     ----------------- ------------------------- ---------------
	 1.  rcu_read_lock()
	 2.                    enters synchronize_rcu()
	 3.                                               rcu_read_lock()
	 4.  rcu_read_unlock()
	 5.                     exits synchronize_rcu()
	 6.                                              rcu_read_unlock()
读者、更新者和回收器之间的 RCU API 通信
由于 synchronize_rcu 是必须弄清楚读者何时完成的 API,因此它的实现是 RCU 的关键。为了使 RCU 在除最密集读取的情况之外的所有情况下都变得有用,synchronize_rcu 的开销也必须非常小。
或者,同步_rcu 可以注册一个回调函数,在所有正在进行的 RCU 读取侧关键区段完成后调用,而不是阻塞。这种回调变体在 Linux 内核中被称为 call_rcu id
rcu_assign_pointer id - 更新器使用此函数将新值分配给由 RCU 保护的指针,以便安全地将值的更改从更新器传达给读取器。此函数返回新值,并执行给定 CPU 架构所需的任何 内存屏障 指令。也许更重要的是,它用于记录哪些指针受 RCU 保护。
rcu_dereference id - 读取器使用此函数获取由 RCU 保护的指针,它返回一个值,然后可以安全地取消引用。它还执行编译器或 CPU 所需的任何指令,例如,gcc 的易失性强制转换、C/C++11 的 memory_order_consume 加载或旧 DEC Alpha CPU 所需的内存屏障指令。rcu_dereference 返回的值仅在包含的 RCU 读取侧关键区段内有效。与 rcu_assign_pointer 一样,rcu_dereference 的一个重要功能是记录哪些指针受 RCU 保护。

RCU 基础设施观察 rcu_read_lockrcu_read_unlocksynchronize_rcucall_rcu调用的时间顺序,以便确定何时 (1) synchronize_rcu 调用可以返回给调用者,以及 (2) call_rcu 回调可以被调用。RCU 基础设施的高效实现大量使用批处理,以便将开销分摊到相应 API 的多次使用上。


⚙️ 内部

kernel/rcusrc


📖 参考资料

避免锁:读复制更新 doc
RCU 概念 doc
RCU 初始化


互斥量
[edit | edit source]

⚲ API

linux/mutex.hinc
linux/completion.hinc
mutex id 拥有者和使用约束,比信号量更容易调试
rt_mutex id 带有优先级继承 (PI) 支持的阻塞互斥锁
ww_mutex id 伤口/等待互斥量:带有死锁避免功能的阻塞互斥锁
rw_semaphore id 读写信号量
percpu_rw_semaphoreid
completion id - 使用完成来同步具有 ISR 和任务或两个任务的任务。
wait_for_completionid
completeid


💾 历史

semaphore id - 如果可能,使用互斥量代替信号量
linux/semaphore.hinc
linux/rwsem.hinc


📖 参考资料

完成 - “等待完成”屏障 API doc
互斥量 API 参考 doc
LWN:完成事件

每个 CPU 本地锁

[edit | edit source]
local_lock idpreempt_disable id
local_lock_irqsave idlocal_irq_save id
等等


在正常的可抢占内核中,local_lock 调用 preempt_disable id。在 RT 可抢占内核中,local_lock 调用 migrate_disable idspin_lock id


⚲ API

linux/local_lock.hinc


📖 参考资料

在可抢占内核下进行正确的锁定 doc
内核中的本地锁


💾 历史:在内核版本 2.6 之前,Linux 禁用中断来实现短关键区段。从 2.6 及更高版本开始,Linux 是完全可抢占的。

自旋锁

[edit | edit source]

自旋锁是一种锁,它会导致试图获取它的线程简单地在循环中等待 (“自旋”),同时反复检查锁是否可用。由于线程保持活动状态但没有执行有用的任务,因此使用这种锁是一种忙等待。一旦获取,自旋锁通常会一直保持,直到被显式释放,尽管在某些实现中,如果等待的线程 (持有锁的线程) 阻塞或“进入睡眠”,它们可能会被自动释放。

自旋锁通常在内核中使用,因为如果线程可能只被阻塞很短时间,它们效率很高。但是,如果自旋锁保持较长时间,它们就会变得浪费,因为它们可能会阻止其他线程运行并需要重新调度。👁 例如 kobj_kset_join id 使用自旋锁来保护对链表的访问。

内核抢占的启用和禁用取代了单处理器系统上的自旋锁 (禁用 CONFIG_SMP id)。大多数自旋锁在 CONFIG_PREEMPT_RT id 内核中成为睡眠锁。


📖 参考资料

spinlock_tid
raw_spinlock_tid
bit_spin_lockid
自旋锁简介
排队自旋锁


顺序锁 (顺序锁的简称) 是一种特殊的锁定机制,用于在 Linux 中支持两个并行操作系统例程之间共享变量的快速写入。当写入器数量很少时,它是读者-写入器问题的特殊解决方案。

它是一种读写一致的机制,可以避免写入器饥饿问题。一个 seqlock_t id 包含用于保存序列计数器 seqcount_t id/seqcount_spinlock_t 的存储,以及一个锁。该锁用于支持两个写入器之间的同步,计数器用于指示读取器的一致性。除了更新共享数据外,写入器还会在获取锁后和释放锁前递增序列计数器。读取器在读取共享数据之前和之后读取序列计数器。如果序列计数器在任一时刻都是奇数,则写入器已获取锁,而数据正在被读取,它可能已更改。如果序列计数器不同,则写入器已更改数据,而它正在被读取。在这两种情况下,读取器只需重试 (使用循环) 直到它们在之前和之后读取到相同的偶数序列计数器。

💾 历史:语义在 2.5.59 版本中稳定下来,并且存在于 2.6.x 稳定内核系列中。顺序锁由 Stephen Hemminger 开发,最初称为 frlocks,基于 Andrea Arcangeli 的早期工作。第一个实现是在 x86-64 时间代码中,在那里需要与用户空间同步,而无法使用真正的锁。


⚲ API

seqlock_tid
DEFINE_SEQLOCK idseqlock_init idread_seqlock_excl idwrite_seqlock id
seqcount_tid
seqcount_init id, read_seqcount_begin id, read_seqcount_retry id, write_seqcount_begin id, write_seqcount_end id
linux/seqlock.hinc

👁 示例:mount_lock id,定义于 fs/namespace.c src


📖 参考资料

序列计数器和顺序锁 doc
SeqLock

自旋锁或睡眠锁

[edit | edit source]
在服务器上 在抢占式实时系统上
spinlock_t, raw_spinlock_t rt_mutex_base, rt_spin_lock, 睡眠
rwlock_t 自旋 睡眠
local_lock preempt_disable migrate_disable, rt_spin_lock, 睡眠


低级

[edit | edit source]

编译器可能会优化掉或重新排序对变量的写操作,从而导致多个线程并发访问变量时出现意外行为。

⚲ API

asm-generic/rwonce.h inc – 阻止编译器合并或重新获取读写操作。
linux/compiler.hinc
barrier id – 阻止编译器重新排序屏障周围的指令
asm-generic/barrier.h inc – 通用屏障定义
arch/x86/include/asm/barrier.h src – 强制严格的 CPU 排序
mb id – 确保屏障之前的内存操作在屏障之后的任何内存操作开始之前完成


📚 进一步阅读

volatile – 阻止编译器优化
内存屏障 – 对内存操作强制执行排序约束

时间

[edit | edit source]

⚲ UAPI

uapi/linux/time.hinc
timespec id — 纳秒级分辨率
timeval id — 微秒级分辨率
时区id
...
uapi/linux/time_types.hinc
__kernel_timespec id — 纳秒级分辨率,用于系统调用
...

⚲ API

linux/time.hinc
tmid
get_timespec64id
...
linux/ktime.hinc
ktime_t id — 用于内核时间值的纳秒标量表示
ktime_subid
...
linux/timekeeping.hinc
ktime_get id, ktime_get_ns id
ktime_get_realid
...
linux/time64.hinc
timespec64id
time64_tid
ns_to_timespec64id
timespec64_subid
ktime_to_timespec64id
...
uapi/linux/rtc.hinc
linux/jiffies.hinc


⚙️ 内部

kernel/timesrc


📖 参考资料

ktime 访问器 doc
时钟源、时钟事件、sched_clock() 和延迟计时器 doc
时间和计时器例程 doc
2038 年问题

⚙️ 锁定内部机制

kernel/lockingsrc
timer_list id wait_queue_head_t id
原子操作 doc
asm-generic/atomic.hinc
linux/atomic/atomic-instrumented.hinc
atomic_dec_and_testid ...
kernel/locking/locktorture.c src – 基于模块的锁定折磨测试工具


📚 锁定引用

锁定 doc
锁类型及其规则 doc
睡眠锁 doc
mutex id, rt_mutex id, semaphore id, rw_semaphore id, ww_mutex id, percpu_rw_semaphore id
在抢占式实时系统上:local_lock, spinlock_t, rwlock_t
自旋锁 doc:
raw_spinlock_t, 位自旋锁
在非抢占式实时系统上:spinlock_t, rwlock_t
锁定不可靠指南 doc
同步(计算机科学)
同步原语
无滴答(全动态滴答), CONFIG_NO_HZ_FULL id

中断

[edit | edit source]

中断是指由硬件或软件发出的信号,通知处理器需要立即处理的事件。中断提醒处理器发生了需要立即处理的紧急情况,因此需要中断处理器当前正在执行的代码。处理器会响应中断,暂停当前活动,保存状态,并执行一个称为中断处理程序(或中断服务例程,ISR)的函数来处理事件。中断是暂时的,中断处理程序完成后,处理器将恢复正常活动。

中断有两种类型:硬件中断和软件中断。硬件中断由设备使用,用于通知操作系统它们需要处理。例如,按下键盘上的键或移动鼠标会触发硬件中断,导致处理器读取按键或鼠标位置。与软件中断不同,硬件中断是异步的,可能在指令执行过程中发生,因此在编程时需要格外注意。启动硬件中断的动作称为中断请求 - IRQ(⚙️ do_IRQ id)。

软件中断是由处理器本身的异常情况或指令集中导致中断的特殊指令执行引起的。前者通常称为*陷阱*(⚙️ do_trap id)或*异常*,用于处理程序执行期间发生的错误或事件,这些事件足够异常,无法在程序本身内处理。例如,如果处理器的算术逻辑单元被命令将一个数字除以零,则此不可能的要求会导致*除零异常*(⚙️ X86_TRAP_DE id),可能导致计算机放弃计算或显示错误消息。软件中断指令的功能类似于子程序调用,并用于各种目的,例如请求低级系统软件(如设备驱动程序)的服务。例如,计算机通常使用软件中断指令与磁盘控制器通信,以请求读取或写入磁盘上的数据。

每个中断都有自己的中断处理程序。硬件中断的数量受处理器中断请求 (IRQ) 线的数量限制,但可能存在数百种不同的软件中断。


⚲ API

/proc/interrupts
man 1 irqtop – 用于显示内核中断信息的实用程序
irqbalance – 在多处理器系统上将硬件中断分配到各个处理器
有许多方法可以请求 ISR,其中两种方法
devm_request_threaded_irq id – 为具有线程化 ISR 的受管设备分配中断线的首选函数
request_irq idfree_irq id – 用于添加和删除中断线处理程序的旧的常用函数
linux/interrupt.h inc – 主要的中断支持头文件
irqaction id – 包含处理程序函数
linux/irq.hinc
irq_dataid
include/linux/irqflags.hinc
irqs_disabledid
local_irq_saveid ...
local_irq_disableid ...
linux/irqdesc.hinc
irq_descid
linux/irqdomain.hinc
irq_domain id – 硬件中断编号转换对象
irq_domain_get_irq_dataid
linux/msi.h inc消息信号中断
msi_descid
结构体的结构
irq_desc id 是以下内容的容器
irq_dataid
irq_common_dataid
irqaction id 列表


⚙️ 内部

kernel/irq/settings.hsrc
kernel/irqsrc
kernel/irq/internals.hsrc
ls /sys/kernel/debug/irq/domains/
x86_vector_domain idx86_vector_domain_ops id
irq_chipid


📖 参考资料

IRQs doc
irq_domain 中断编号映射库 doc
Linux 通用 IRQ 处理 doc
消息信号中断:MSI 驱动程序指南 doc
锁类型及其规则 doc
硬 IRQ 上下文 doc
中断

👁 示例

dummy_irq_chip id – 虚拟中断芯片实现
lib/locking-selftest.csrc

IRQ 亲和性

[edit | edit source]

⚲ API

/proc/irq/default_smp_affinity
/proc/irq/*/smp_affinity 和 /proc/irq/*/smp_affinity_list

常用类型和函数

struct irq_affinity id – 用于自动 irq 亲和性分配的描述,参见 devm_platform_get_irqs_affinity id
struct irq_affinity_desc id – 中断亲和性描述符,参见 irq_update_affinity_desc idirq_create_affinity_masks id
irq_set_affinityid
irq_get_affinity_maskid
irq_can_set_affinityid
irq_set_affinity_hintid
irqd_affinity_is_managedid
irq_data_get_affinity_maskid
irq_data_get_effective_affinity_maskid
irq_data_update_effective_affinityid
irq_set_affinity_notifierid
irq_affinity_notifyid
irq_chip_set_affinity_parentid
irq_set_vcpu_affinityid


🛠️ 工具

irqbalance – 将硬件中断分配到各个 CPU

📖 参考资料

SMP IRQ 亲和性 doc
IRQ 亲和性,LF
managed_irq 内核参数@LKML
irqaffinity 内核参数@LKML


📚 进一步阅读

IDT – 中断描述符表

延迟工作

[edit | edit source]

调度程序上下文

[edit | edit source]

线程化 IRQ

[edit | edit source]

⚲ API

devm_request_threaded_irq idrequest_threaded_irq id

ISR 应返回 IRQ_WAKE_THREAD 以运行线程函数

⚙️ 内部

setup_irq_thread idirq_thread id
kernel/irq/manage.csrc

📖 参考资料

request_threaded_irq doc


工作

[edit | edit source]

工作是 workqueue 的包装器

⚲ API

linux/workqueue.hinc
work_struct idINIT_WORK idschedule_work id
delayed_work idINIT_DELAYED_WORK idschedule_delayed_work idcancel_delayed_work_sync id


👁 示例用法 samples/ftrace/sample-trace-array.c src

⚙️ 内部机制:system_wq id

工作队列

[edit | edit source]

⚲ API

linux/workqueue.hinc
workqueue_struct idalloc_workqueue idqueue_work id


⚙️ 内部

workqueue_init id, create_worker id, pool_workqueue id
kernel/workqueue.csrc


📖 参考资料

并发管理工作队列 doc

中断上下文

[edit | edit source]
linux/irq_work.h inc – 从硬中断上下文中将回调排队和运行的框架
samples/trace_printk/trace-printk.csrc

定时器

[edit | edit source]
软中断定时器
[edit | edit source]

此定时器是用于具有节拍分辨率的定期任务的软中断

⚲ API

linux/timer.hinc
timer_list id, DEFINE_TIMER id, timer_setup id
mod_timer id — 以节拍设置过期时间。
del_timerid

⚙️ 内部

kernel/time/timer.csrc

👁 示例

input_enable_softrepeat idinput_start_autorepeat id
高分辨率定时器
[edit | edit source]

⚲ API

linux/hrtimer.hinc
hrtimer id, hrtimer.function — 回调
hrtimer_init id, hrtimer_cancel id
hrtimer_start id 启动一个具有纳秒级分辨率的定时器


👁 示例 watchdog_enable id


⚙️ 内部

CONFIG_HIGH_RES_TIMERSid
kernel/time/hrtimer.c src


📚 HR 定时器参考

高分辨率定时器 doc
hrtimers - 用于高分辨率内核定时器的子系统 doc
高分辨率定时器和动态节拍设计说明 doc


📚 定时器参考

定时器 doc
为定时器过期选择更好的 CPU

任务

[edit | edit source]

tasklet 是一个软中断,用于时间关键操作

⚲ API 已弃用,有利于线程化中断:devm_request_threaded_irq id

tasklet_struct id, tasklet_init id, tasklet_schedule id


⚙️ 内部实现:tasklet_action_common id HI_SOFTIRQ, TASKLET_SOFTIRQ


软中断

[edit | edit source]

软中断是内部系统工具,不应直接使用。使用 tasklet 或线程化中断

⚲ API

cat /proc/softirqs
open_softirq id 注册 softirq_action id


⚙️ 内部

kernel/softirq.csrc


⚲ API

linux/interrupt.hinc


📖 参考资料

延迟中断简介(软中断、任务和工作队列)
软中断、任务和工作队列
定时器和时间管理
延迟工作,linux-kernel-labs
第 7 章。时间、延迟和延迟工作

CPU 特定

[edit | edit source]

🖱️ GUI

tuna – 用于调整运行进程的程序


⚲ API

cat /proc/cpuinfo
/sys/devices/system/cpu/
/sys/cpu/
/sys/fs/cgroup/cpu/
grep -i cpu /proc/self/status
rdmsr – 用于读取 CPU 机器特定寄存器 (MSR) 的工具
man 1 lscpu – 显示有关 CPU 架构的信息


linux/arch_topology.h inc – 架构特定的 CPU 拓扑信息
linux/cpu.h inc – 通用 CPU 定义
linux/cpu_cooling.hinc
linux/cpu_pm.hinc
linux/cpufeature.hinc
linux/cpufreq.hinc
linux/cpuhotplug.h inc – CPU 热插拔状态
linux/cpuidle.h inc – 用于 CPU 空闲功耗管理的通用框架
linux/peci-cpu.hinc
linux/sched/cpufreq.h inc – cpufreq 驱动程序和调度程序之间的接口
linux/sched/cputime.h inc – cputime 计费 API


⚙️ 内部

drivers/cpufreqsrc
intel_pstateid
acpi_cpufreq_driverid
drivers/cpuidlesrc


缓存

[edit | edit source]
linux/cacheflush.hinc
arch/x86/include/asm/cacheflush.h src: clflush_cache_range id
linux/cache.hinc
arch/x86/include/asm/cache.hsrc


⚙️ 内部

arch/x86/mm/pat/set_memory.csrc


📖 参考资料

工作状态电源管理 doc
https://www.thinkwiki.org/wiki/How_to_use_cpufrequtils
cpufreq ltp

本章介绍 Linux 内核的多处理和多核方面。

Linux SMP 的关键概念和功能包括

  • 对称性:在 SMP 系统中,所有处理器都被认为是相同的,没有硬件层次结构,与使用协处理器相反。
  • 负载均衡:Linux 内核采用负载均衡机制将任务均匀地分配到可用 CPU 内核。这样可以防止任何一个内核不堪重负,而其他内核却闲置。
  • 并行性:SMP 允许并行处理,其中多个线程或进程可以在不同的 CPU 内核上同时执行。这可以显着提高为利用多个线程而设计的应用程序的执行速度。
  • 线程调度:Linux 内核调度程序负责确定哪些线程或进程在哪些 CPU 内核上运行以及运行多长时间。它旨在通过最大限度地减少竞争和最大限度地利用 CPU 来优化性能。
  • 共享内存:在 SMP 系统中,所有 CPU 内核通常共享相同的物理内存空间。这允许在不同内核上运行的进程和线程更有效地通信和共享数据。
  • NUMA – 非一致内存访问: 在较大的 SMP 系统中,由于内存库和处理器的物理排列,内存访问时间可能不一致。Linux 具有有效处理 NUMA 架构的机制,允许进程调度到更靠近其相关内存的 CPU 上。
  • 缓存一致性: SMP 系统需要机制来确保所有 CPU 内核对内存的一致视图。缓存一致性协议确保对共享内存位置的更改正确传播到所有内核。
  • 可扩展性: SMP 系统可以扩展到包含更多 CPU 内核,从而增强系统的整体计算能力。但是,随着内核数量的增加,可能会出现与内存访问、争用和内核之间通信相关的挑战。
  • 内核和用户空间: 在用户空间运行的 Linux 应用程序可以利用 SMP,而无需了解底层硬件细节。内核处理 CPU 内核的管理和资源分配。


🗝️ 关键术语

亲和性是指将进程或线程分配到特定 CPU 内核。这有助于控制哪些 CPU 执行任务,通过减少内核之间的数据移动来提高性能。可以使用系统调用或命令来管理。亲和性可以用 CPU 位掩码表示: cpumask_t id 或 CPU 亲和性列表: cpulist_parse id.


⚲ API

ps -PLe – 列出线程以及线程上次执行所在的处理器(第三列 PSR)。
man 1 taskset – 设置或检索进程的 CPU 亲和性
man 2 getcpu – 确定调用线程正在运行的 CPU 和 NUMA 节点
man 7 cpuset – 将进程限制在处理器和内存节点子集
man 8 chcpu – 配置 CPU
man 3 CPU_SET – 用于操作 CPU 集的宏
grep Cpus_allowed /proc/self/status
man 2 sched_setaffinity man 2 sched_getaffinity – 设置和获取线程的 CPU 亲和性掩码
sched_setaffinity id
set_cpus_allowed_ptr id – 更改任务亲和性掩码的常用内核函数
linux/smp.hinc
linux/cpu.hinc
linux/group_cpus.h inc: group_cpus_evenly id – 根据 NUMA/CPU 局部性均匀地对所有 CPU 进行分组
linux/cpuset.h inc – cpuset 接口
linux/cpu_rmap.h inc – CPU 亲和性反向映射支持
linux/cpumask_types.hinc
struct cpumask, cpumask_t id – CPU 位图,可能非常大
cpumask_var_t id – 本地 cpumask 变量的类型,请参见 alloc_cpumask_var id, free_cpumask_var id.
linux/cpumask.h inc – Cpumasks 提供一个位图,适合表示系统中的 CPU 集,每个 CPU 编号一个位位置
asm-generic/percpu.hinc
linux/percpu-defs.h inc – 每个 CPU 区域的基本定义
this_cpu_ptrid
linux/percpu.hinc
linux/percpu-refcount.hinc
linux/percpu-rwsem.hinc
linux/preempt.hinc
migrate_disable id, migrate_enable id
/sys/bus/cpu
每个 CPU local_lock


⚙️ 内部

boot_cpu_init id 激活第一个 CPU
smp_prepare_cpus id 在启动期间初始化其余 CPU
cpuset_initid
cpu_numberid
cpus_mask idtask_struct id 的亲和性
CONFIG_SMPid
CONFIG_CPUSETSid
CONFIG_CPU_ISOLATIONid
CONFIG_NUMAid
trace/events/percpu.hinc
IPI – 处理器间中断
trace/events/ipi.hinc
kernel/irq/ipi.csrc
ipi_send_single id, ipi_send_mask id ...
drivers/base/cpu.c src – CPU 驱动程序模型子系统支持
kernel/cpu.csrc
smpboot
linux/smpboot.hinc
kernel/smpboot.csrc
arch/x86/kernel/smpboot.csrc
lib/group_cpus.csrc


🛠️ 工具

irqbalance – 将硬件中断分配到各个 CPU
man 8 numactl – 控制进程或共享内存的 NUMA 策略


📖 参考资料

cgroup v1 的 CPUSETS doc
命令行参数中的 CPU 列表 doc
nohz_full 清理维护操作。cpumasks id 用于 housekeeping_nohz_full_setup id 中的 tick、wq、timer、rcu、misc 和 kthread
isolcpus 清理维护操作。cpumasks id 用于 housekeeping_isolcpus_setup id 中的 tick、domain 和 managed_irq


📚 进一步阅读

CPU 隔离技术现状,LPC'23
CPU 分区
调度程序域 doc – 调度程序在调度程序域内平衡 CPU(调度组)
CPU 隔离
isolcpus @LKML
nohz_full @LKML
TuneD 插件的功能

CPU 热插拔

[edit | edit source]

⚲ API

linux/cpuhotplug.h inc – CPU 热插拔状态
cpuhp_setup_stateid ...
cpuhp_setup_state_multiid
cpuhp_setup_state_nocallsid

⚙️ 内部

kernel/cpu.csrc
cpuhp_hp_statesid
boot_cpu_hotplug_initid
kernel/irq/cpuhotplug.csrc


📖 参考资料

内核中的 CPU 热插拔 doc


📚 进一步阅读

cpuhotplug @LKML

内存屏障 (MB) 是用于确保 SMP 环境中内存操作正确排序的同步机制。它们在维护不同 CPU 内核或处理器之间共享数据的 consistency 和正确性方面起着至关重要的作用。MB 阻止编译器或 CPU 对内存访问指令进行意外和潜在的 harmful 重排序,这会导致并发软件系统中的数据损坏和竞争条件。

⚲ API

man 2 membarrier
asm-generic/barrier.hinc
mb id, rmb id, wmb id
smp_mb id, smp_rmb id, smp_wmb id


⚙️ 内部

arch/x86/include/asm/barrier.hsrc
kernel/sched/membarrier.csrc


📖 参考资料

内存屏障 doc

架构

[edit | edit source]

Linux CPU 架构指的是与 Linux 操作系统兼容的各种中央处理器 (CPU) 类型。Linux 被设计为可在各种 CPU 架构上运行,使其能够在各种设备上使用,从智能手机到服务器和超级计算机。每种架构都有其独特的特性、优点和设计注意事项。

架构按系列(例如 x86、ARM)、长整型 大小(例如 CONFIG_32BIT idCONFIG_64BIT id)进行分类。


一些针对不同 CPU 架构有不同实现的函数

do_boot_cpu id > start_secondary id > cpu_init id
setup_arch idstart_thread idget_current idcurrent id

⚲ API

BITS_PER_LONG id__BITS_PER_LONG id,

⚙️ 架构内部

archsrc
x86
CONFIG_X86id
arch/x86src
drivers/platform/x86src
https://lwn.net/Kernel/Index/#Architectures-x86
ARM
CONFIG_ARMid
arch/arm srcARM 架构 doc
https://lwn.net/Kernel/Index/#Architectures-ARM
arch/arm64 srcARM64 架构 doc
特定于架构的初始化


📖 参考资料

CPU 架构 doc
特定于 x86 doc
x86_64 支持 doc


📚 关于多任务、调度和 CPU 的进一步阅读

bcc/ebpf CPU 和调度程序工具
  1. a b Malte Skarupke。 "测量互斥锁、自旋锁以及 Linux 调度程序到底有多糟糕".
华夏公益教科书