跳转到内容

多任务功能

来自Wikibooks,开放世界的开放书籍


多任务
进程
线程或任务
同步
调度器
中断核心
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。Clone创建一个子进程,该子进程可能与父进程共享其执行上下文的一部分。它通常用于实现线程(尽管程序员通常会使用更高级别的接口,如man 7 pthreads,它是在clone之上实现的)。


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(系统调用)

进程间通信

[编辑 | 编辑源代码]

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

信号 (⚲ API ↪ ⚙️ 实现)

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

🔧 待补充: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 参见零拷贝
Express 数据路径 PF_XDP id
Unix 域套接字 PF_UNIX id
内存映射文件 man 2 mmapksys_mmap_pgoff id
Sys V IPC
消息队列
信号量
共享内存:man 2 shmgetman 2 shmctlman 2 shmatman 2 shmdt


📖 参考

进程间通信

man 7 sysvipc

线程或任务

[编辑 | 编辑源代码]

在 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

调度器

[编辑 | 编辑源代码]

调度器 是操作系统中决定在特定时间点运行哪个进程的部分。它通常具有暂停正在运行的进程、将其移至运行队列末尾并启动新进程的能力。

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

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

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

与旧的 O(1) 调度器一样,CFS 使用一个称为“睡眠公平性”的概念,它将睡眠或等待的任务视为与运行队列上的任务等效。这意味着大部分时间都在等待用户输入或其他事件的交互式任务,在需要时可以获得同等比例的 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
调度id
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 和调度器工具


抢占是指系统中断正在运行的任务以切换到另一个任务的能力。这对于确保高优先级任务获得必要的 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

等待队列

[编辑 | 编辑源代码]

内核中的等待队列是一种数据结构,它允许一个或多个进程等待(休眠),直到发生感兴趣的事情。它们在整个内核中被用于等待可用内存、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
处理等待队列

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

用户空间同步

[编辑 | 编辑源代码]

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

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


Futex 的基本操作仅基于两个核心操作futex_wait idfutex_wake id,尽管实现具有更多操作以用于更专业的情况。

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


⚲ API

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

⚙️ 内部实现:kernel/futex.c src

📖 参考

Futex
man 7 futex
Futex API 参考 doc
futex ltp


文件锁定

[编辑 | 编辑源代码]

⚲ API:man 2 flock


信号量

[编辑 | 编辑源代码]

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

⚲ API

man 2 semget
man 2 semctl
man 2 semget


⚙️ 内部实现:ipc/sem.c src

内核空间同步

[编辑 | 编辑源代码]

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

睡眠锁

[编辑 | 编辑源代码]
读-复制-更新
[编辑 | 编辑源代码]

解决读者-写者问题的常用机制是读-复制-更新 (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 的开销也必须非常小。
或者,synchronize_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 初始化


互斥量
[编辑 | 编辑源代码]

⚲ API

linux/mutex.hinc
linux/completion.hinc
mutex id 具有所有者和使用约束,比信号量更容易调试
rt_mutex id 带有优先级继承 (PI) 支持的阻塞互斥锁
ww_mutex id 缠绕/等待互斥量:带有死锁避免的阻塞互斥锁
rw_semaphore id 读者-写者信号量
percpu_rw_semaphoreid
completion id - 使用 completion 用于中断服务程序和任务或两个任务之间的同步。
wait_for_completionid
completeid


💾 历史

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


📖 参考

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

每个 CPU 本地锁

[编辑 | 编辑源代码]
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 是完全抢占式的。

自旋锁

[编辑 | 编辑源代码]

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

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

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


📖 参考

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


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

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

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


⚲ API

seqlock_tid
DEFINE_SEQLOCK id, seqlock_init id, read_seqlock_excl id, write_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

自旋锁或睡眠锁

[编辑 | 编辑源代码]
在服务器上 在抢占式 RT 上
spinlock_t, raw_spinlock_t rt_mutex_base, rt_spin_lock, 睡眠
rwlock_t 自旋 睡眠
local_lock preempt_disable migrate_disable, rt_spin_lock, 睡眠


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

⚲ 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 – 防止编译器进行优化
内存屏障 – 对内存操作实施排序约束

⚲ 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
在抢占式 RT 上:local_lock, spinlock_t, rwlock_t
自旋锁 doc:
raw_spinlock_t, 位自旋锁
在非抢占式 RT 上:spinlock_t, rwlock_t
不可靠的锁定指南 doc
同步(计算机科学)
同步原语

无tick内核 (全dynticks), CONFIG_NO_HZ_FULL id

一个中断 是由硬件或软件发出的处理器信号,指示需要立即关注的事件。中断提醒处理器注意需要中断处理器当前正在执行的代码的高优先级条件。处理器通过暂停其当前活动、保存其状态并执行称为中断处理程序(或中断服务例程,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 id, free_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 id, x86_vector_domain_ops id
irq_chipid


📖 参考

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

👁 示例

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

IRQ 亲和性

[编辑 | 编辑源代码]

⚲ 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 id, irq_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 – 中断描述符表

延迟工作

[编辑 | 编辑源代码]

调度程序上下文

[编辑 | 编辑源代码]

线程化 IRQ

[编辑 | 编辑源代码]

⚲ API

devm_request_threaded_irq id, request_threaded_irq id

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

⚙️ 内部

setup_irq_thread id, irq_thread id
kernel/irq/manage.csrc

📖 参考

request_threaded_irq doc


work 是 workqueue 的包装器

⚲ API

linux/workqueue.hinc
work_struct id, INIT_WORK id, schedule_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

工作队列

[编辑 | 编辑源代码]

⚲ API

linux/workqueue.hinc
工作队列相关的结构体和函数:workqueue_struct idalloc_workqueue idqueue_work id


⚙️ 内部

workqueue_init idcreate_worker idpool_workqueue id
kernel/workqueue.csrc


📖 参考

并发管理的工作队列 doc

中断上下文

[编辑 | 编辑源代码]
linux/irq_work.h inc – 用于从硬中断上下文中排队和运行回调的框架
samples/trace_printk/trace-printk.csrc

定时器

[编辑 | 编辑源代码]
软中断定时器
[编辑 | 编辑源代码]

此定时器是用于具有jiffies分辨率的周期性任务的软中断。

⚲ API

linux/timer.hinc
定时器相关的结构体和函数:timer_list idDEFINE_TIMER idtimer_setup id
mod_timer id — 设置以jiffies为单位的超时时间。
del_timerid

⚙️ 内部

kernel/time/timer.csrc

👁 示例

input_enable_softrepeat idinput_start_autorepeat id
高分辨率定时器
[编辑 | 编辑源代码]

⚲ API

linux/hrtimer.hinc
hrtimer id,hrtimer.function — 回调函数
hrtimer_init idhrtimer_cancel id
hrtimer_start id 以纳秒分辨率启动定时器


👁 示例 watchdog_enable id


⚙️ 内部

CONFIG_HIGH_RES_TIMERSid
kernel/time/hrtimer.c src


📚 高分辨率定时器相关参考

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


📚 定时器相关参考

定时器 doc
更好的定时器超时CPU选择

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

⚲ API 已被弃用,建议使用线程化中断:devm_request_threaded_irq id

tasklet 相关的结构体和函数:tasklet_struct idtasklet_init idtasklet_schedule id


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


软中断

[编辑 | 编辑源代码]

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

⚲ API

cat /proc/softirqs
open_softirq id 注册 softirq_action id


⚙️ 内部

kernel/softirq.csrc


⚲ API

linux/interrupt.hinc


📖 参考

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

CPU特定

[编辑 | 编辑源代码]

🖱️ 图形界面

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


linux/cacheflush.hinc
arch/x86/include/asm/cacheflush.h srcclflush_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 idfree_cpumask_var id
linux/cpumask.h inc – Cpumasks提供了一个位图,适合表示系统中的CPU集,每个CPU编号一个位位置
asm-generic/percpu.hinc
linux/percpu-defs.h inc – percpu区域的基本定义
this_cpu_ptrid
linux/percpu.hinc
linux/percpu-refcount.hinc
linux/percpu-rwsem.hinc
linux/preempt.hinc
migrate_disable idmigrate_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 idipi_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 用于tick、wq、timer、rcu和kthread在housekeeping_nohz_full_setup id中。
isolcpus 清除管家工作。cpumasks id 用于tick、domain和managed_irq在housekeeping_isolcpus_setup id中。


📚 进一步阅读

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

CPU热插拔

[编辑 | 编辑源代码]

⚲ 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 热插拔 文档


📚 进一步阅读

LKML 上的 cpuhotplug

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

⚲ 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


📖 参考

内存屏障 文档

体系结构

[编辑 | 编辑源代码]

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 id, start_thread id, get_current id, current 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 源代码, ARM 架构 文档
https://lwn.net/Kernel/Index/#Architectures-ARM
arch/arm64 源代码, ARM64 架构 文档
特定于体系结构的初始化


📖 参考

CPU 体系结构 文档
特定于 x86 的 文档
x86_64 支持 文档


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

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